diff --git a/README.md b/README.md new file mode 100644 index 00000000000..7469ed5de5e --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# SmartThings Public Github Repo + +An official list of SmartApps and Device Types from SmartThings. + +Here are some links to help you get started coding right away: + +* [Github-specific Documentation](http://docs.smartthings.com/en/latest/tools-and-ide/github-integration.html) +* [Full Documentation](http://docs.smartthings.com) +* [IDE & Simulator](http://ide.smartthings.com) +* [Community Forums](http://community.smartthings.com) + +Follow us on the web: + +* Twitter: http://twitter.com/smartthingsdev +* Facebook: http://facebook.com/smartthingsdevelopers diff --git a/devicetypes/capabilities/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy b/devicetypes/capabilities/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy new file mode 100644 index 00000000000..14ec809fab6 --- /dev/null +++ b/devicetypes/capabilities/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy @@ -0,0 +1,38 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Acceleration Sensor Capability", namespace: "capabilities", author: "SmartThings") { + capability "Acceleration Sensor" + } + + simulator { + status "active": "acceleration:active" + status "inactive": "acceleration:inactive" + } + + tiles { + standardTile("acceleration", "device.acceleration", width: 2, height: 2) { + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + } + + main "acceleration" + details "acceleration" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/alarm-capability.src/alarm-capability.groovy b/devicetypes/capabilities/alarm-capability.src/alarm-capability.groovy new file mode 100644 index 00000000000..21847129b8e --- /dev/null +++ b/devicetypes/capabilities/alarm-capability.src/alarm-capability.groovy @@ -0,0 +1,73 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Alarm Capability", namespace: "capabilities", author: "SmartThings") { + capability "Alarm" + } + + simulator { + // reply messages + ["strobe","siren","both","off"].each { + reply "$it": "alarm:$it" + } + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label:'off', action:'alarm.strobe', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + state "strobe", label:'strobe!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + state "siren", label:'siren!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + standardTile("strobe", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "off", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc" + state "siren", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc" + state "strobe", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13" + state "both", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13" + } + standardTile("siren", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "off", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc" + state "strobe", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc" + state "siren", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13" + state "both", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + } + main "alarm" + details(["alarm","strobe","siren","test","off"]) + } +} + +def strobe() { + "strobe" +} + +def siren() { + "siren" +} + +def both() { + "both" +} + +def off() { + "off" +} + +// Parse incoming device messages to generate events +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/button-capability.src/button-capability.groovy b/devicetypes/capabilities/button-capability.src/button-capability.groovy new file mode 100644 index 00000000000..636a3ba6df5 --- /dev/null +++ b/devicetypes/capabilities/button-capability.src/button-capability.groovy @@ -0,0 +1,46 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Button Capability", namespace: "capabilities", author: "SmartThings") { + capability "Button" + } + + simulator { + status "button 1 pushed": "command: 2001, payload: 01" + status "button 1 held": "command: 2001, payload: 15" + status "button 2 pushed": "command: 2001, payload: 29" + status "button 2 held": "command: 2001, payload: 3D" + status "wakeup": "command: 8407, payload: " + } + tiles { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + main "button" + details "button" + } +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + // log.debug("Parsed '$description' to $results") + return results +} diff --git a/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy b/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy new file mode 100644 index 00000000000..1aa6f2225b0 --- /dev/null +++ b/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy @@ -0,0 +1,37 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Contact Sensor Capability", namespace: "capabilities", author: "SmartThings") { + capability "Contact Sensor" + } + + simulator { + status "open": "contact:open" + status "closed": "contact:closed" + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + } + main "contact" + details "contact" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/illuminance-measurement-capability.src/illuminance-measurement-capability.groovy b/devicetypes/capabilities/illuminance-measurement-capability.src/illuminance-measurement-capability.groovy new file mode 100644 index 00000000000..23c4485c1a5 --- /dev/null +++ b/devicetypes/capabilities/illuminance-measurement-capability.src/illuminance-measurement-capability.groovy @@ -0,0 +1,39 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Illuminance Measurement Capability", namespace: "capabilities", author: "SmartThings") { + capability "Illuminance Measurement" + } + + simulator { + for (i in [0,5,10,15,20,30,40,50,100,200,300,400,600,800,1000]) { + status "${i} lux": "illuminance:${i}" + } + } + + tiles { + valueTile("illuminance", "device.illuminance", width: 2, height: 2) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + main(["illuminance"]) + details(["illuminance"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) +{ + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/lock-capability.src/lock-capability.groovy b/devicetypes/capabilities/lock-capability.src/lock-capability.groovy new file mode 100644 index 00000000000..634e7c6c34b --- /dev/null +++ b/devicetypes/capabilities/lock-capability.src/lock-capability.groovy @@ -0,0 +1,55 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Lock Capability", namespace: "capabilities", author: "SmartThings") { + capability "Lock" + } + + simulator { + status "locked": "lock:locked" + status "unlocked": "lock:unlocked" + + reply "lock": "lock:locked" + reply "unlock": "lock:unlocked" + } + + tiles { + standardTile("toggle", "device.lock", width: 2, height: 2) { + state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821" + } + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked" + } + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked" + } + + main "toggle" + details(["toggle", "lock", "unlock", "refresh"]) + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def lock() { + "lock" +} + +def unlock() { + "unlock" +} diff --git a/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy b/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy new file mode 100644 index 00000000000..9fa0a6b8915 --- /dev/null +++ b/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy @@ -0,0 +1,50 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Momentary Capability", namespace: "capabilities", author: "SmartThings") { + capability "Momentary" + } + + // simulator metadata + simulator { + // status messages + // none + + // reply messages + reply "'on','delay 2000','off'": "switch:off" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + main "switch" + details "switch" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def push() { + ['on','delay 2000','off'] +} + +def off() { + 'off' +} diff --git a/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy b/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy new file mode 100644 index 00000000000..52cc2b99223 --- /dev/null +++ b/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy @@ -0,0 +1,37 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Motion Sensor Capability", namespace: "capabilities", author: "SmartThings") { + capability "Motion Sensor" + } + + simulator { + status "active": "motion:active" + status "inactive": "motion:inactive" + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + } + main "motion" + details "motion" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy b/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy new file mode 100644 index 00000000000..d90d7fa5543 --- /dev/null +++ b/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy @@ -0,0 +1,37 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Presence Sensor Capability", namespace: "capabilities", author: "SmartThings") { + capability "Presence Sensor" + } + + simulator { + status "present": "presence: present" + status "not present": "presence: not present" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2) { + state("not present", label:'not present', icon:"st.presence.tile.not-present", backgroundColor:"#ffffff") + state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#53a7c0") + } + main "presence" + details "presence" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/relative-humidity-measurement-capability.src/relative-humidity-measurement-capability.groovy b/devicetypes/capabilities/relative-humidity-measurement-capability.src/relative-humidity-measurement-capability.groovy new file mode 100644 index 00000000000..9a1cda42fc2 --- /dev/null +++ b/devicetypes/capabilities/relative-humidity-measurement-capability.src/relative-humidity-measurement-capability.groovy @@ -0,0 +1,39 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Relative Humidity Measurement Capability", namespace: "capabilities", author: "SmartThings") { + capability "Relative Humidity Measurement" + } + + // simulator metadata + simulator { + for (int i = 0; i <= 100; i += 10) { + status "${i}%": "humidity: ${i}" + } + } + + // UI tile definitions + tiles { + valueTile("humidity", "device.humidity", width: 2, height: 2) { + state "humidity", label:'${currentValue}%', unit:"" + } + } +} + +// Parse incoming device messages to generate events +// Parse incoming device messages to generate events +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"%") +} diff --git a/devicetypes/capabilities/switch-capability.src/switch-capability.groovy b/devicetypes/capabilities/switch-capability.src/switch-capability.groovy new file mode 100644 index 00000000000..dc2070f9ab5 --- /dev/null +++ b/devicetypes/capabilities/switch-capability.src/switch-capability.groovy @@ -0,0 +1,52 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Switch Capability", namespace: "capabilities", author: "SmartThings") { + capability "Switch" + } + + // simulator metadata + simulator { + // status messages + status "on": "switch:on" + status "off": "switch:off" + + // reply messages + reply "on": "switch:on" + reply "off": "switch:off" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + main "switch" + details "switch" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def on() { + 'on' +} + +def off() { + 'off' +} diff --git a/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy b/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy new file mode 100644 index 00000000000..15230bc6269 --- /dev/null +++ b/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy @@ -0,0 +1,80 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Switch Level Capability", namespace: "capabilities", author: "SmartThings") { + capability "Switch Level" + } + + simulator { + status "on": "switch:on" + status "off": "switch:off" + + reply "on":"on" + reply "off":"off" + + [5,10,25,33,50,66,75,99].each { + status "$it%": "switch:on,level:$it" + } + reply "setLevel: 0":"switch:off,level:0" + (1..99).each { + reply "setLevel: $it":"switch:on,level:$it" + } + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2) { + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" + state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" + } + controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) { + state "level", action:"setLevel" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh", icon:"st.secondary.refresh" + } + + main "switch" + details "switch", "levelSliderControl", "refresh" + } +} + +def parse(String description) { + log.trace description + def pairs = description.split(",") + def result = [] + pairs.each { + def pair = it.split(":") + result << createEvent(name: pair[0].trim(), value: pair[1].trim()) + } + log.trace result + result +} + +def on() { + 'on' +} + +def off() { + 'off' +} + +def setLevel(value) { + "setLevel: $value" +} + +def refresh() { + 'refresh' +} diff --git a/devicetypes/capabilities/temperature-measurement-capability.src/temperature-measurement-capability.groovy b/devicetypes/capabilities/temperature-measurement-capability.src/temperature-measurement-capability.groovy new file mode 100644 index 00000000000..99b7628924f --- /dev/null +++ b/devicetypes/capabilities/temperature-measurement-capability.src/temperature-measurement-capability.groovy @@ -0,0 +1,50 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Temperature Measurement Capability", namespace: "capabilities", author: "SmartThings") { + capability "Temperature Measurement" + } + + // simulator metadata + simulator { + for (int i = 0; i <= 100; i += 10) { + status "${i} F": "temperature:$i" + } + } + + // UI tile definitions + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + main "temperature" + details "temperature" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"F") +} diff --git a/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy b/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy new file mode 100644 index 00000000000..113b4a58e0d --- /dev/null +++ b/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy @@ -0,0 +1,181 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Thermostat Capability", namespace: "capabilities", author: "SmartThings") { + capability "Thermostat" + } + + // simulator metadata + simulator { + ["on","off","heat","cool","emergency heat"].each { + status "$it": "thermostatMode:$it" + } + + ["on","auto","circulate"].each { + status "fan $it": "thermostatFanMode:$it" + } + + [60,68,72].each { + status "heat $it": "heatingSetpoint:$it" + } + + [72,76,80,85].each { + status "cool $it": "coolingSetpoint:$it" + } + + [40,58,62,70,74,78,82,86].each { + status "temp $it": "temperature:$it" + } + + // reply messages + //reply "2502": "command: 2503, payload: FF" + ["on","off","heat","cool","emergency heat"].each { + reply "thermostatMode:$it": "thermostatMode:$it" + } + ["on","auto","circulate"].each { + reply "thermostatFanMode:$it": "thermostatFanMode:$it" + } + for (n in 60..90) { + reply "heatingSetpoint:${n}": "heatingSetpoint:$n" + reply "heatingSetpoint:${n}.0": "heatingSetpoint:$n" + } + for (n in 60..90) { + reply "coolingSetpoint:${n}": "coolingSetpoint:$n" + reply "coolingSetpoint:${n}.0": "coolingSetpoint:$n" + } + } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue}° heat', unit: "F", backgroundColor:"#ffffff" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"thermostat.emergencyHeat", backgroundColor:"#ffffff" + state "emergencyHeat", label:'${name}', action:"thermostat.heat", backgroundColor:"#e86d13" + state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffc000" + state "cool", label:'${name}', action:"thermostat.off", backgroundColor:"#269bd2" + } + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff" + state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff" + state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff" + } + + main "temperature" + details(["temperature", "heatingSetpoint", "coolingSetpoint", "mode", "fanMode"]) + } +} + +def parse(String description) +{ + def pair = description.split(":") + def map = createEvent(name: pair[0].trim(), value: pair[1].trim()) + def result = [map] + + if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) { + def map2 = [ + name: "thermostatSetpoint", + unit: "F" + ] + if (map.name == "thermostatMode") { + if (map.value == "cool") { + map2.value = device.latestValue("coolingSetpoint") + log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}" + } + else { + map2.value = device.latestValue("heatingSetpoint") + log.info "THERMOSTAT, latest heating setpoint = ${map2.value}" + } + } + else { + def mode = device.latestValue("thermostatMode") + log.info "THERMOSTAT, latest mode = ${mode}" + if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) { + map2.value = map.value + map2.unit = map.unit + } + } + if (map2.value != null) { + log.debug "THERMOSTAT, adding setpoint event: $map" + result << createEvent(map2) + } + } + log.debug "Parse returned ${result?.descriptionText}" + result +} + +def setHeatingSetpoint(Double degreesF) { + "heatingSetpoint:$degreesF" +} + +def setCoolingSetpoint(Double degreesF) { + "coolingSetpoint:$degreesF" +} + +def setThermostatMode(String value) { + "thermostatMode:$value" +} + +def setThermostatFanMode(String value) { + "thermostatFanMode:$value" +} + +def off() { + "thermostatMode:off" +} + +def heat() { + "thermostatMode:heat" +} + +def emergencyHeat() { + "thermostatMode:emergency heat" +} + +def cool() { + "thermostatMode:cool" +} + +def fanOn() { + "thermostatFanMode:on" +} + +def fanAuto() { + "thermostatMode:auto" +} + +def fanCirculate() { + "thermostatMode:circulate" +} + +def poll() { + null +} + diff --git a/devicetypes/capabilities/three-axis-capability.src/three-axis-capability.groovy b/devicetypes/capabilities/three-axis-capability.src/three-axis-capability.groovy new file mode 100644 index 00000000000..882cdd97f0c --- /dev/null +++ b/devicetypes/capabilities/three-axis-capability.src/three-axis-capability.groovy @@ -0,0 +1,42 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Three Axis Capability", namespace: "capabilities", author: "SmartThings") { + capability "Three Axis" + } + + simulator { + status "x,y,z: 0,0,0": "threeAxis:0,0,0" + status "x,y,z: 1000,0,0": "threeAxis:1000,0,0" + status "x,y,z: 0,1000,0": "threeAxis:0,1000,0" + status "x,y,z: 0,0,1000": "xthreeAxis:0,0,1000" + status "x,y,z: -1000,0,0": "threeAxis:-1000,0,0" + status "x,y,z: 0,-1000,0": "threeAxis:0,-1000,0" + status "x,y,z: 0,0,-1000": "xthreeAxis:0,0,-1000" + } + + tiles { + valueTile("3axis", "device.threeAxis", decoration: "flat") { + state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") + } + + main "3axis" + details "3axis" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy b/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy new file mode 100644 index 00000000000..5b338e53721 --- /dev/null +++ b/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy @@ -0,0 +1,38 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Water Sensor Capability", namespace: "capabilities", author: "SmartThings") { + capability "Water Sensor" + } + + simulator { + status "wet": "water:wet" + status "dry": "water:dry" + } + + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + + main "water" + details "water" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/com-obycode/obything-music.src/obything-music.groovy b/devicetypes/com-obycode/obything-music.src/obything-music.groovy new file mode 100644 index 00000000000..b244e3e8eaf --- /dev/null +++ b/devicetypes/com-obycode/obything-music.src/obything-music.groovy @@ -0,0 +1,376 @@ +/** + * ObyThing Music + * + * Copyright 2014 obycode + * + * 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. + * + */ + +import groovy.json.JsonSlurper + +metadata { + definition (name: "ObyThing Music", namespace: "com.obycode", author: "obycode") { + capability "Music Player" + capability "Refresh" + capability "Switch" + + command "playTrackAtVolume", ["string","number"] + command "playTrackAndResume", ["string","number","number"] + command "playTextAndResume", ["string","number"] + command "playTrackAndRestore", ["string","number","number"] + command "playTextAndRestore", ["string","number"] + command "playSoundAndTrack", ["string","number","json_object","number"] + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + // Main + standardTile("main", "device.status", width: 1, height: 1, canChangeIcon: true) { + state "paused", label:'Paused', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff" + state "playing", label:'Playing', action:"music Player.pause", icon:"st.Electronics.electronics19", nextState:"paused", backgroundColor:"#79b821" + } + + // Row 1 + standardTile("nextTrack", "device.status", width: 1, height: 1, decoration: "flat") { + state "next", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn", backgroundColor:"#ffffff" + } + standardTile("playpause", "device.status", width: 1, height: 1, decoration: "flat") { + state "default", label:'', action:"music Player.play", icon:"st.sonos.play-btn", nextState:"playing", backgroundColor:"#ffffff" + state "playing", label:'', action:"music Player.pause", icon:"st.sonos.pause-btn", nextState:"paused", backgroundColor:"#ffffff" + state "paused", label:'', action:"music Player.play", icon:"st.sonos.play-btn", nextState:"playing", backgroundColor:"#ffffff" + } + standardTile("previousTrack", "device.status", width: 1, height: 1, decoration: "flat") { + state "previous", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn", backgroundColor:"#ffffff" + } + + // Row 2 + standardTile("airplay", "device.switch", width: 1, height: 1, decoration: "flat", canChangeIcon: true) { + state "on", label:'AirPlay On', action:"switch.off", icon:"st.Electronics.electronics14", nextState:"off", backgroundColor:"#ffffff" + state "off", label:'AirPlay Off', action:"switch.on", icon:"st.Electronics.electronics16", nextState:"on", backgroundColor:"#ffffff" + } + standardTile("status", "device.status", width: 1, height: 1, decoration: "flat", canChangeIcon: true) { + state "playing", label:'Playing', action:"music Player.pause", icon:"st.Electronics.electronics19", nextState:"paused", backgroundColor:"#ffffff" + state "stopped", label:'Stopped', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff" + state "paused", label:'Paused', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff" + } + standardTile("mute", "device.mute", inactiveLabel: false, decoration: "flat") { + state "unmuted", label:"Mute", action:"music Player.mute", icon:"st.custom.sonos.unmuted", backgroundColor:"#ffffff", nextState:"muted" + state "muted", label:"Unmute", action:"music Player.unmute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff", nextState:"unmuted" + } + + // Row 3 + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"music Player.setLevel", backgroundColor:"#ffffff" + } + + // Row 4 - Disable this for now until we get communication back to hub working +// valueTile("currentSong", "device.trackDescription", inactiveLabel: true, height:1, width:3, decoration: "flat") { +// state "default", label:'${currentValue}', backgroundColor:"#ffffff" +// } + + // Row 5 + standardTile("refresh", "device.status", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", backgroundColor:"#ffffff" + } + + main "main" + + details([ + "previousTrack","playpause","nextTrack", + "airplay","status","mute", + "levelSliderControl", +// "currentSong", + "refresh" + ]) + } + +// mappings { +// path("/obything/:message") { +// action: [ +// GET: "updateState" +// ] +// } +// } +} + +// parse events into attributes +def parse(String description) { + //log.debug "Parsing '${description}'" + def map = stringToMap(description) + if (map.headers && map.body) { //got device info response + if (map.body) { + def bodyString = new String(map.body.decodeBase64()) + //log.debug "body = $bodyString" + def slurper = new JsonSlurper() + def result = slurper.parseText(bodyString) + if (result.containsKey("volume")) { + log.debug "setting volume to ${result.volume}" + sendEvent(name: "level", value: result.volume) + } + if (result.containsKey("mute")) { + log.debug "setting mute to ${result.mute}" + sendEvent(name: "mute", value: result.mute) + } + if (result.containsKey("status")) { + log.debug "setting status to ${result.status}" + sendEvent(name: "status", value: result.status) + } + if (result.containsKey("trackData")) { + def json = new groovy.json.JsonBuilder(result.trackData) + log.debug "setting trackData to ${json.toString()}" + sendEvent(name: "trackData", value: json.toString()) + } + if (result.containsKey("trackDescription")) { + log.debug "setting trackDescription info to ${result.trackDescription}" + sendEvent(name: "trackDescription", value: result.trackDescription) + } + if (result.containsKey("airplay")) { + log.debug "setting airplay to ${result.airplay}" + sendEvent(name: "switch", value: result.airplay) + } + if (result.containsKey("playlists")) { + result.playlists.each() { + sendEvent(name: "trackData", value: "{\"station\": \"${it}\"}") + } + } + } + } +} + +def updateState() { + log.debug "updateState: ${params.message}" +} + +def installed() { +// subscribeAction("/subscribe") + refresh() +} + +// handle commands +def refresh() { + log.debug "refreshing" + //def address = getCallBackAddress() + //sendCommand("subscribe=$address") + sendCommand("refresh") +} + +def on() { + log.debug "Turn AirPlay on" + sendCommand("airplay=on") +} + +def off() { + log.debug "Turn AirPlay off" + sendCommand("airplay=off") +} + +def play() { + log.debug "Executing 'play'" + sendCommand("command=play") +} + +def pause() { + log.debug "Executing 'pause'" + sendCommand("command=pause") +} + +def stop() { + log.debug "Executing 'stop'" + sendCommand("command=stop") +} + +def nextTrack() { + log.debug "Executing 'nextTrack'" + sendCommand("command=next") +} + +def playTrack(String uri, metaData="") { + log.debug "Executing 'playTrack'" + sendCommand("playTrack&track=${uri}") +} + +def playTrack(Map trackData) { + log.debug "Executing 'playTrack'" + sendCommand("playlist=${trackData.station}") +} + +def setLevel(value) { + log.debug "Executing 'setLevel' to $value" + sendCommand("volume=$value") +} + +def playText(String msg) { + log.debug "Executing 'playText'" + sendCommand("say=$msg") +} + +def mute() { + log.debug "Executing 'mute'" + sendCommand("command=mute") +} + +def previousTrack() { + log.debug "Executing 'previousTrack'" + sendCommand("command=previous") +} + +def unmute() { + log.debug "Executing 'unmute'" + sendCommand("command=unmute") +} + +def setTrack(String uri, metaData="") { + log.debug "Executing 'setTrack'" + sendCommand("track=$uri") +} + +def resumeTrack() { + log.debug "Executing 'resumeTrack'" + // TODO: handle 'resumeTrack' command +} + +def restoreTrack() { + log.debug "Executing 'restoreTrack'" + // TODO: handle 'restoreTrack' command +} + +def playTrackAtVolume(String uri, volume) { + log.trace "playTrackAtVolume($uri, $volume)" + sendCommand("playTrack&track=${uri}&volume=${volume}") +} + +def playTrackAndResume(uri, duration, volume=null) { + log.debug "playTrackAndResume($uri, $duration, $volume)" + def cmd = "playTrack&track=${uri}&resume" + if (volume) { + cmd += "&volume=${volume}" + } + sendCommand(cmd) +} + +def playTextAndResume(text, volume=null) +{ + log.debug "playTextAndResume($text, $volume)" + def sound = textToSpeech(text) + playTrackAndResume(sound.uri, (sound.duration as Integer) + 1, volume) +} + +def playTrackAndRestore(uri, duration, volume=null) { + log.debug "playTrackAndResume($uri, $duration, $volume)" + def cmd = "playTrack&track=${uri}&restore" + if (volume) { + cmd += "&volume=${volume}" + } + sendCommand(cmd) +} + +def playTextAndRestore(text, volume=null) +{ + log.debug "playTextAndResume($text, $volume)" + def sound = textToSpeech(text) + playTrackAndRestore(sound.uri, (sound.duration as Integer) + 1, volume) +} + +def playURL(theURL) { + log.debug "Executing 'playURL'" + sendCommand("url=$theURL") +} + + +def playSoundAndTrack(soundUri, duration, trackData, volume=null) { + log.debug "playSoundAndTrack($uri, $duration, $trackData, $volume)" + def cmd = "playTrack&track=${soundUri}&playlist=${trackData.station}" + if (volume) { + cmd += "&volume=${volume}" + } + sendCommand(cmd) +} + +// Private functions used internally +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress() { + def parts = device.deviceNetworkId.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +private sendCommand(command) { + def path = "/post.html" + + def headers = [:] + headers.put("HOST", getHostAddress()) + headers.put("Content-Type", "application/x-www-form-urlencoded") + + def method = "POST" + + def result = new physicalgraph.device.HubAction( + method: method, + path: path, + body: command, + headers: headers + ) + + result +} + +private getPlaylists() { + log.debug "in getPlaylists!!!" + def path = "/get.html?list=playlists" + + def headers = [:] + headers.put("GET", getHostAddress()) + headers.put("Content-Type", "application/x-www-form-urlencoded") + + def method = "GET" + + def result = new physicalgraph.device.HubAction( + method: method, + path: path, + headers: headers + ) + + result +} + +private getCallBackAddress() +{ + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") +} + +private subscribeAction(path, callbackPath="") { + def address = device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") + def parts = device.deviceNetworkId.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + ip = ip + ":" + port + + def result = new physicalgraph.device.HubAction( + method: "SUBSCRIBE", + path: path, + headers: [ + HOST: ip, + CALLBACK: "", + NT: "upnp:event", + TIMEOUT: "Second-3600"]) + result +} diff --git a/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy b/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy new file mode 100644 index 00000000000..00916de0e50 --- /dev/null +++ b/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy @@ -0,0 +1,63 @@ +/** + * netatmo-basestation + * + * Copyright 2014 Brian Steere + * + * 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: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") { + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + + attribute "carbonDioxide", "string" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label: '${currentValue}°', unit:"F", backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "default", label:'${currentValue}%', unit:"Humidity" + } + valueTile("carbonDioxide", "device.carbonDioxide", inactiveLabel: false) { + state "default", label:'${currentValue}ppm', unit:"CO2" + } + standardTile("refresh", "device.pressure", inactiveLabel: false, decoration: "flat") { + state "default", action:"device.poll", icon:"st.secondary.refresh" + } + main "temperature" + details(["temperature", "humidity", "carbonDioxide", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + +} + +def poll() { + parent.poll() +} \ No newline at end of file diff --git a/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy b/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy new file mode 100644 index 00000000000..f0a844c5d8a --- /dev/null +++ b/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy @@ -0,0 +1,81 @@ +/** + * netatmo-basestation + * + * Copyright 2014 Brian Steere + * + * 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: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") { + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + + attribute "carbonDioxide", "string" + attribute "noise", "string" + attribute "pressure", "string" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label: '${currentValue}°', backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "humidity", label:'${currentValue}%', unit:"Humidity" + } + valueTile("carbonDioxide", "device.carbonDioxide", inactiveLabel: false) { + state "carbonDioxide", label:'${currentValue}ppm', unit:"CO2", backgroundColors: [ + [value: 600, color: "#44B621"], + [value: 999, color: "#ffcc00"], + [value: 1000, color: "#e86d13"] + ] + } + valueTile("noise", "device.noise", inactiveLabel: false) { + state "noise", label:'${currentValue}db', unit:"Noise" + } + valueTile("pressure", "device.pressure", inactiveLabel: false) { + state "pressure", label:'${currentValue}mbar', unit:"Pressure" + } + standardTile("refresh", "device.pressure", inactiveLabel: false, decoration: "flat") { + state "default", action:"device.poll", icon:"st.secondary.refresh" + } + main(["temperature", "humidity", "carbonDioxide", "noise", "pressure"]) + details(["temperature", "humidity", "carbonDioxide", "noise", "pressure", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + // TODO: handle 'humidity' attribute + // TODO: handle 'temperature' attribute + // TODO: handle 'carbonDioxide' attribute + // TODO: handle 'noise' attribute + // TODO: handle 'pressure' attribute + +} + +def poll() { + parent.poll() +} + diff --git a/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy b/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy new file mode 100644 index 00000000000..45ef2b266cb --- /dev/null +++ b/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy @@ -0,0 +1,64 @@ +/** + * netatmo-outdoor + * + * Copyright 2014 Brian Steere + * + * 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: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") { + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label: '${currentValue}°', backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "humidity", label:'${currentValue}%', unit:"Humidity" + } + standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "default", action:"device.poll", icon:"st.secondary.refresh" + } + main (["temperature", "humidity"]) + details(["temperature", "humidity", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + // TODO: handle 'humidity' attribute + // TODO: handle 'temperature' attribute + // TODO: handle 'carbonDioxide' attribute + // TODO: handle 'noise' attribute + // TODO: handle 'pressure' attribute + +} + +def poll() { + parent.poll() +} + diff --git a/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy b/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy new file mode 100644 index 00000000000..a882f23d596 --- /dev/null +++ b/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy @@ -0,0 +1,55 @@ +/** + * netatmo-basestation + * + * Copyright 2014 Brian Steere + * + * 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: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") { + attribute "rain", "number" + attribute "rainSumHour", "number" + attribute "rainSumDay", "number" + attribute "units", "string" + + command "poll" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + valueTile("rain", "device.rain", width: 2, height: 2, inactiveLabel: false) { + state "default", label:'${currentValue}' + } + valueTile("rainSumHour", "device.rainSumHour", inactiveLabel: false) { + state "default", label:'${currentValue}\nhour' + } + valueTile("rainSumDay", "device.rainSumDay", inactiveLabel: false) { + state "default", label:'${currentValue}\nday' + } + standardTile("refresh", "device.rain", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.poll", icon:"st.secondary.refresh" + } + main (["rain", "rainSumHour", "rainSumDay"]) + details(["rain", "rainSumHour", "rainSumDay", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def poll() { + parent.poll() +} diff --git a/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy b/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy new file mode 100644 index 00000000000..4d15594201e --- /dev/null +++ b/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy @@ -0,0 +1,122 @@ +/** + * Jawbone-User + * + * Author: juano23@gmail.com + * Date: 2013-08-15 + */ + // for the UI + +metadata { + // Automatically generated. Make future change here. + definition (name: "Jawbone User", namespace: "juano2310", author: "juano23@gmail.com") { + capability "Refresh" + capability "Polling" + capability "Button" + capability "Sleep Sensor" + capability "Step Sensor" + } + + simulator { + status "sleeping": "sleeping: 1" + status "not sleeping": "sleeping: 0" + } + + tiles { + standardTile("sleeping", "device.sleeping", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false) { + state("sleeping", label: "Sleeping", icon:"st.Bedroom.bedroom12", backgroundColor:"#ffffff") + state("not sleeping", label: "Awake", icon:"st.Health & Wellness.health12", backgroundColor:"#79b821") + } + standardTile("steps", "device.steps", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false) { + state("steps", label: '${currentValue} Steps', icon:"st.Health & Wellness.health11", backgroundColor:"#ffffff") + } + standardTile("goal", "device.goal", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { + state("goal", label: '${currentValue} Steps', icon:"st.Health & Wellness.health5", backgroundColor:"#ffffff") + } + standardTile("refresh", "device.steps", inactiveLabel: false, decoration: "flat") { + state "default", action:"polling.poll", icon:"st.secondary.refresh" + } + main "steps" + details(["steps", "goal", "sleeping", "refresh"]) + } +} + +def generateSleepingEvent(boolean sleeping) { + log.debug "Here in generateSleepingEvent!" + def value = formatValue(sleeping) + def linkText = getLinkText(device) + def descriptionText = formatDescriptionText(linkText, sleeping) + def handlerName = getState(sleeping) + + def results = [ + name: "sleeping", + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName + ] + + sendEvent (results) + + log.debug "Generating Sleep Event: ${results}" + + + def results2 = [ + name: "button", + value: "held", + unit: null, + linkText: linkText, + descriptionText: "${linkText} button was pressed", + handlerName: "buttonHandler", + data: [buttonNumber: 1], + isStateChange: true + ] + + + log.debug "Generating Button Event: ${results2}" + + sendEvent (results2) +} + + +def poll() { + log.debug "Executing 'poll'" + def results = parent.pollChild(this) + return null +} + +def setMemberId (String memberId) { + log.debug "MemberId = ${memberId}" + state.jawboneMemberId = memberId +} + +def getMemberId () { + log.debug "MemberId = ${state.jawboneMemberId}" + return(state.jawboneMemberId) +} + +def uninstalled() { + log.debug "Uninstalling device, then app" + parent.app.delete() +} + +private String formatValue(boolean sleeping) { + if (sleeping) + return "sleeping" + else + return "not sleeping" +} + +private formatDescriptionText(String linkText, boolean sleeping) { + if (sleeping) + return "$linkText is sleeping" + else + return "$linkText is not sleeping" +} + +private getState(boolean sleeping) { + if (sleeping) + return "sleeping" + else + return "not sleeping" +} diff --git a/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy b/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy new file mode 100644 index 00000000000..3d7d73cdd83 --- /dev/null +++ b/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy @@ -0,0 +1,710 @@ +/** + * Aeon Home Energy Meter + C3 + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + // Automatically generated. Make future change here. + definition(name: "Aeon Home Energy Meter + C3", namespace: "smartthings", author: "SmartThings") { + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Sensor" + + command "reset" + +// fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60" + } + + // simulator metadata + simulator { + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + } + + // tile definitions + tiles { + valueTile("power", "device.power", decoration: "flat") { + state "default", label: '${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label: '${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label: 'reset kWh', action: "reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") { + state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure" + } + + PLATFORM_graphTile(name: "powerGraph", attribute: "device.power") + + main(["power", "energy"]) + details(["powerGraph", "power", "energy", "reset", "refresh", "configure"]) + } +} + +// ======================================================== +// PREFERENCES +// ======================================================== + +preferences { + input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily" + input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions() +} + +def selectedGraphPrecision() { + graphPrecision ?: "Daily" +} + +def selectedGraphType() { + graphType ?: "line" +} + +// ======================================================== +// MAPPINGS +// ======================================================== + +mappings { + path("/graph/:attribute") { + action: + [ + GET: "renderGraph" + ] + } + path("/graphDataSizes") { // for testing. remove before publishing + action: + [ + GET: "graphDataSizes" + ] + } +} + +def graphDataSizes() { // for testing. remove before publishing + state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData -> + attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData -> + dateTypes[dateTypeData.key] = dateTypeData.value.size() + dateTypes + } + attributes + } +} + +// ======================================================== +// Z-WAVE +// ======================================================== + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + log.debug "Parse returned ${result?.descriptionText}" + + PLATFORM_migrateGraphDataIfNeeded() + PLATFORM_storeData(result.name, result.value) + + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { + if (cmd.scale == 0) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } else { + [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def refresh() { + delayBetween([ + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def reset() { + // No V1 available + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} + +def configure() { + def cmd = delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // combined power in watts + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // combined energy in kWh + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report + zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format() // every 5 min + ]) + log.debug cmd + cmd +} + +// ======================================================== +// GRAPH RENDERING // written by developer. Alter at will +// ======================================================== + +def renderGraph() { + + def data = PLATFORM_fetchGraphData(params.attribute) + + def totalData = data*.runningSum + + def xValues = data*.unixTime + + def yValues = [ + Total: [color: "#49a201", data: totalData, type: selectedGraphType()] + ] + + PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts") +} + +// TODO: // ======================================================== +// TODO: // PLATFORM CODE !!! DO NOT ALTER !!! +// TODO: // ======================================================== + +// ======================================================== +// PLATFORM TILES +// ======================================================== + +def PLATFORM_graphTile(Map tileParams) { + def cleanAttribute = tileParams.attribute - "device." - "capability." + htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams) +} + +// ======================================================== +// PLATFORM GRAPH RENDERING +// ======================================================== + +private PLATFORM_graphTypeOptions() { + [ + "line", // DEFAULT + "spline", + "step", + "area", + "area-spline", + "area-step", + "bar", + "scatter", + "pie", + "donut", + "gauge", + ] +} + +private PLATFORM_renderGraph(graphParams) { + + String attribute = graphParams.attribute + List xValues = graphParams.xValues + Map yValues = graphParams.yValues + String focus = graphParams.focus ?: "" + String label = graphParams.label ?: "" + + /* + def xValues = [1, 2] + + def yValues = [ + High: [type: "spline", data: [5, 6], color: "#bc2323"], + Low: [type: "spline", data: [0, 1], color: "#153591"] + ] + + Available type values: + line // DEFAULT + spline + step + area + area-spline + area-step + bar + scatter + pie + donut + gauge + +*/ + + def graphData = PLATFORM_buildGraphData(xValues, yValues, label) + + def legendData = yValues*.key + def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified" + def flowColumn = focus ?: yValues ? yValues.keySet().first() : null + + def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph" + renderHTML(htmlTitle) { html -> + html.head { + """ + + + + + + + + + + + """ + } + html.body { + """ +
+
+ + + """ + } + } +} + +private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") { + + /* + def xValues = [1, 2] + + def yValues = [ + High: [type: "spline", data: [5, 6], color: "#bc2323"], + Low: [type: "spline", data: [0, 1], color: "#153591"] + ] + */ + + [ + interaction: [ + enabled: false + ], + bindto : '#chart', + padding : [ + left : 30, + right : 30, + bottom: 0, + top : 0 + ], + legend : [ + show: false, +// hide : false,//(yValues.keySet().size() < 2), +// position: 'inset', +// inset: [ +// anchor: "top-right" +// ], +// item: [ +// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing" +// ] + ], + data : [ + x : "x", + columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data }, + types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total }, + colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total } + ], + axis : [ + x: [ + type: 'timeseries', + tick: [ + centered: true, + culling : [max: 7], + fit : true, + format : PLATFORM_getGraphDateFormat() +// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript + ] + ], + y: [ + label : label, + padding: [ + top: 50 + ] + ] + ] + ] +} + +private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) { + // https://github.com/mbostock/d3/wiki/Time-Formatting + def graphDateFormat + switch (dateType) { + case "Live": + graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM + break + case "Hourly": + graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM + break + case "Daily": + graphDateFormat = "%a" // abbreviated weekday name + break + case "Monthly": + graphDateFormat = "%b" // abbreviated month name + break + case "Annually": + graphDateFormat = "%y" // year without century as a decimal number [00,99] + break + } + graphDateFormat +} + +private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) { + def graphDateFunction = "function(date) { return date; }" + switch (dateType) { + case "Live": + graphDateFunction = """ + function(date) { + return.getMinutes(); + } + """ + break; + case "Hourly": + graphDateFunction = """ function(date) { + var hour = date.getHours(); + if (hour == 0) { + return String(/12 am/).substring(1).slice(0,-1); + } else if (hour > 12) { + return hour -12 + String(/ pm/).substring(1).slice(0,-1); + } else { + return hour + String(/ am/).substring(1).slice(0,-1); + } + }""" + break + case "Daily": + graphDateFunction = """ function(date) { + var day = date.getDay(); + switch(day) { + case 0: return String(/Sun/).substring(1).slice(0,-1); + case 1: return String(/Mon/).substring(1).slice(0,-1); + case 2: return String(/Tue/).substring(1).slice(0,-1); + case 3: return String(/Wed/).substring(1).slice(0,-1); + case 4: return String(/Thu/).substring(1).slice(0,-1); + case 5: return String(/Fri/).substring(1).slice(0,-1); + case 6: return String(/Sat/).substring(1).slice(0,-1); + } + }""" + break + case "Monthly": + graphDateFunction = """ function(date) { + var month = date.getMonth(); + switch(month) { + case 0: return String(/Jan/).substring(1).slice(0,-1); + case 1: return String(/Feb/).substring(1).slice(0,-1); + case 2: return String(/Mar/).substring(1).slice(0,-1); + case 3: return String(/Apr/).substring(1).slice(0,-1); + case 4: return String(/May/).substring(1).slice(0,-1); + case 5: return String(/Jun/).substring(1).slice(0,-1); + case 6: return String(/Jul/).substring(1).slice(0,-1); + case 7: return String(/Aug/).substring(1).slice(0,-1); + case 8: return String(/Sep/).substring(1).slice(0,-1); + case 9: return String(/Oct/).substring(1).slice(0,-1); + case 10: return String(/Nov/).substring(1).slice(0,-1); + case 11: return String(/Dec/).substring(1).slice(0,-1); + } + }""" + break + case "Annually": + graphDateFunction = """ + function(date) { + return.getFullYear(); + } + """ + break + } + groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction) +} + +private jsEscapeString(str = "") { + "String(/${str}/).substring(1).slice(0,-1);" +} + +private PLATFORM_fetchGraphData(attribute) { + + log.debug "PLATFORM_fetchGraphData(${attribute})" + + /* + [ + [ + dateString: "2014-12-1", + unixTime: 1421931600000, + min: 0, + max: 10, + average: 5 + ], + ... + ] + */ + + def attributeBucket = state["measure.${attribute}"] ?: [:] + def dateType = selectedGraphPrecision() + attributeBucket[dateType] +} + +// ======================================================== +// PLATFORM DATA STORAGE +// ======================================================== + +private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] } + +private PLATFORM_storeData(attribute, value) { + PLATFORM_graphPrecisionOptions().each { dateType -> + PLATFORM_addDataToBucket(attribute, value, dateType) + } +} + +/* +[ + Hourly: [ + [ + dateString: "2014-12-1", + unixTime: 1421931600000, + min: 0, + max: 10, + average: 5 + ], + ... + ], + ... +] +*/ + +private PLATFORM_addDataToBucket(attribute, value, dateType) { + + def numberValue = value.toBigDecimal() + + def attributeKey = "measure.${attribute}" + def attributeBucket = state[attributeKey] ?: [:] + + def dateTypeBucket = attributeBucket[dateType] ?: [] + + def now = new Date() + def itemDateString = now.format("PLATFORM_get${dateType}Format"()) + def item = dateTypeBucket.find { it.dateString == itemDateString } + + if (!item) { + // no entry for this data point yet, fill with initial values + item = [:] + item.average = numberValue + item.runningSum = numberValue + item.runningCount = 1 + item.min = numberValue + item.max = numberValue + item.unixTime = now.getTime() + item.dateString = itemDateString + + // add the new data point + dateTypeBucket << item + + // clear out old data points + def old = PLATFORM_getOldDateString(dateType) + if (old) { // annual data never gets cleared + dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) } + } + + // limit the size of the bucket. Live data can stack up fast + def sizeLimit = 25 + if (dateTypeBucket.size() > sizeLimit) { + dateTypeBucket = dateTypeBucket[-sizeLimit..-1] + } + + } else { + //re-calculate average/min/max for this bucket + item.runningSum = (item.runningSum.toBigDecimal()) + numberValue + item.runningCount = item.runningCount.toInteger() + 1 + item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger() + + if (item.min == null) { + item.min = numberValue + } else if (numberValue < item.min.toBigDecimal()) { + item.min = numberValue + } + if (item.max == null) { + item.max = numberValue + } else if (numberValue > item.max.toBigDecimal()) { + item.max = numberValue + } + } + + attributeBucket[dateType] = dateTypeBucket + state[attributeKey] = attributeBucket +} + +private PLATFORM_getOldDateString(dateType) { + def now = new Date() + def date + switch (dateType) { + case "Live": + date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour + break + case "Hourly": + date = (now - 1).getTime() + break + case "Daily": + date = (now - 10).getTime() + break + case "Monthly": + date = (now - 30).getTime() + break + case "Annually": + break + } + date +} + +private PLATFORM_getLiveFormat() { "HH:mm:ss" } + +private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" } + +private PLATFORM_getDailyFormat() { "yyyy-MM-dd" } + +private PLATFORM_getMonthlyFormat() { "yyyy-MM" } + +private PLATFORM_getAnnuallyFormat() { "yyyy" } + +// ======================================================== +// PLATFORM GRAPH DATA MIGRATION +// ======================================================== + +private PLATFORM_migrateGraphDataIfNeeded() { + if (!state.hasMigratedOldGraphData) { + def acceptableKeys = PLATFORM_graphPrecisionOptions() + def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet() + needsMigration.each { PLATFORM_migrateGraphData(it) } + state.hasMigratedOldGraphData = true + } +} + +private PLATFORM_migrateGraphData(attribute) { + + log.trace "about to migrate ${attribute}" + + def attributeBucket = state[attribute] ?: [:] + def migratedAttributeBucket = [:] + + attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem -> + + def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find { + "PLATFORM_get${it}Format"().size() == oldDateString.size() + } + + def dateTypeFormat = "PLATFORM_get${dateType}Format"() + + def newBucket = attributeBucket[dateType] ?: [] +/* + def existingNewItem = newBucket.find { it.dateString == oldDateString } + if (existingNewItem) { + newBucket.remove(existingNewItem) + } +*/ + + def newItem = [ + min : oldItem.min, + max : oldItem.max, + average : oldItem.average, + runningSum : oldItem.runningSum, + runningCount: oldItem.runningCount, + dateString : oldDateString, + unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime() + ] + + newBucket << newItem + migratedAttributeBucket[dateType] = newBucket + } + + state[attribute] = migratedAttributeBucket +} diff --git a/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy b/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy new file mode 100644 index 00000000000..8d3abbd9c34 --- /dev/null +++ b/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy @@ -0,0 +1,118 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Aeon Home Energy Meter + * + * Author: SmartThings + * + * Date: 2013-05-30 + */ +metadata { + definition (name: "Aeon Home Energy Meter", namespace: "smartthings", author: "SmartThings") { + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Sensor" + + command "reset" + + fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60" + } + + // simulator metadata + simulator { + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + } + + // tile definitions + tiles { + valueTile("power", "device.power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["power","energy"]) + details(["power","energy", "reset","refresh", "configure"]) + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { + if (cmd.scale == 0) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } + else { + [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def refresh() { + delayBetween([ + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def reset() { + // No V1 available + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} + +def configure() { + def cmd = delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // combined power in watts + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // combined energy in kWh + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report + zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format() // every 5 min + ]) + log.debug cmd + cmd +} diff --git a/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy b/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy new file mode 100644 index 00000000000..2f2c747c61c --- /dev/null +++ b/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy @@ -0,0 +1,192 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Illuminator Module", namespace: "smartthings", author: "SmartThings") { + capability "Energy Meter" + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "reset" + + fingerprint deviceId: "0x1104", inClusters: "0x26,0x32,0x27,0x2C,0x2B,0x70,0x85,0x72,0x86", outClusters: "0x82" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" + state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" + } + controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh", icon:"st.secondary.refresh" + } + + main(["switch", "level"]) + details(["switch", "levelSliderControl","energy","refresh"]) + } +} + +def parse(String description) { + def item1 = [ + canBeCurrentState: false, + linkText: getLinkText(device), + isStateChange: false, + displayed: false, + descriptionText: description, + value: description + ] + def result + def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1, 0x32: 2]) + if (cmd) { + result = createEvent(cmd, item1) + } + else { + item1.displayed = displayed(description, item1.isStateChange) + result = [item1] + } + log.debug "Parse returned ${result?.descriptionText}" + result +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + result[0].descriptionText = "${item1.linkText} is ${item1.value}" + result[0].handlerName = cmd.value ? "statusOn" : "statusOff" + for (int i = 0; i < result.size(); i++) { + result[i].type = "digital" + } + result +} + +def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { + def result = [item1] + + item1.name = "switch" + item1.value = cmd.value ? "on" : "off" + item1.handlerName = item1.value + item1.descriptionText = "${item1.linkText} was turned ${item1.value}" + item1.canBeCurrentState = true + item1.isStateChange = isStateChange(device, item1.name, item1.value) + item1.displayed = item1.isStateChange + + if (cmd.value > 15) { + def item2 = new LinkedHashMap(item1) + item2.name = "level" + item2.value = cmd.value as String + item2.unit = "%" + item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" + item2.canBeCurrentState = true + item2.isStateChange = isStateChange(device, item2.name, item2.value) + item2.displayed = false + result << item2 + } + result +} + +def createEvent(physicalgraph.zwave.commands.meterv2.MeterReport cmd, Map item1) +{ + if (cmd.scale == 0) { + createEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]) + } else if (cmd.scale == 1) { + createEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"]) + } + else { + createEvent([name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]) + } +} + +def createEvent(physicalgraph.zwave.Command cmd, Map map) { + // Handles any Z-Wave commands we aren't interested in + log.debug "UNHANDLED COMMAND $cmd" +} + +def on() { + log.info "on" + delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def off() { + delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value) { + delayBetween ([zwave.basicV1.basicSet(value: value).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value, duration) { + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + zwave.switchMultilevelV2.switchMultilevelSet(value: value, dimmingDuration: dimmingDuration).format() +} + +def poll() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet().format() + ] +} + +def configure() { + delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 8).format(), // energy in kWh + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 0).format(), + zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format() + ]) +} diff --git a/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy b/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy new file mode 100644 index 00000000000..eac86516fe8 --- /dev/null +++ b/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy @@ -0,0 +1,120 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Key Fob", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Button" + capability "Configuration" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x80,0x84,0x85" + } + + simulator { + status "button 1 pushed": "command: 2001, payload: 01" + status "button 1 held": "command: 2001, payload: 15" + status "button 2 pushed": "command: 2001, payload: 29" + status "button 2 held": "command: 2001, payload: 3D" + status "button 3 pushed": "command: 2001, payload: 51" + status "button 3 held": "command: 2001, payload: 65" + status "button 4 pushed": "command: 2001, payload: 79" + status "button 4 held": "command: 2001, payload: 8D" + status "wakeup": "command: 8407, payload: " + } + tiles { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" + state "button 2 pushed", label: "pushed #2", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" + state "button 3 pushed", label: "pushed #3", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" + state "button 4 pushed", label: "pushed #4", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" + state "button 1 held", label: "held #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" + state "button 2 held", label: "held #2", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" + state "button 3 held", label: "held #3", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" + state "button 4 held", label: "held #4", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + main "button" + details(["button", "battery"]) + } +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + // log.debug("Parsed '$description' to $results") + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)] + + def prevBattery = device.currentState("battery") + if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) { + results << response(zwave.batteryV1.batteryGet().format()) + } + results += configurationCmds().collect{ response(it) } + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + return results +} + +def buttonEvent(button, held) { + button = button as Integer + if (held) { + createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) + } else { + createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + Integer button = ((cmd.sceneId + 1) / 2) as Integer + Boolean held = !(cmd.sceneId % 2) + buttonEvent(button, held) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + } else { + map.value = cmd.batteryLevel + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [ descriptionText: "$device.displayName: $cmd", linkText:device.displayName, displayed: false ] +} + +def configurationCmds() { + [ zwave.configurationV1.configurationSet(parameterNumber: 250, scaledConfigurationValue: 1).format(), + zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId).format() ] +} + +def configure() { + def cmd = configurationCmds() + log.debug("Sending configuration: $cmd") + return cmd +} diff --git a/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy b/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy new file mode 100644 index 00000000000..e12be97bca7 --- /dev/null +++ b/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy @@ -0,0 +1,109 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Minimote", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Button" + capability "Configuration" + capability "Sensor" + + fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B", outClusters: "0x26,0x2B" + fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B,0x85,0x84", outClusters: "0x26" // old style with numbered buttons + } + + simulator { + status "button 1 pushed": "command: 2001, payload: 01" + status "button 1 held": "command: 2001, payload: 15" + status "button 2 pushed": "command: 2001, payload: 29" + status "button 2 held": "command: 2001, payload: 3D" + status "button 3 pushed": "command: 2001, payload: 51" + status "button 3 held": "command: 2001, payload: 65" + status "button 4 pushed": "command: 2001, payload: 79" + status "button 4 held": "command: 2001, payload: 8D" + status "wakeup": "command: 8407, payload: " + } + tiles { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + main "button" + details(["button"]) + } +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + // log.debug("Parsed '$description' to $results") + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)] + + results += configurationCmds().collect{ response(it) } + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + + return results +} + +def buttonEvent(button, held) { + button = button as Integer + if (held) { + createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) + } else { + createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + Integer button = ((cmd.sceneId + 1) / 2) as Integer + Boolean held = !(cmd.sceneId % 2) + buttonEvent(button, held) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + Integer button = (cmd.value / 40 + 1) as Integer + Boolean held = (button * 40 - cmd.value) <= 20 + buttonEvent(button, held) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [ descriptionText: "$device.displayName: $cmd", linkText:device.displayName, displayed: false ] +} + +def configurationCmds() { + def cmds = [] + def hubId = zwaveHubNodeId + (1..4).each { button -> + cmds << zwave.configurationV1.configurationSet(parameterNumber: 240+button, scaledConfigurationValue: 1).format() + } + (1..4).each { button -> + cmds << zwave.configurationV1.configurationSet(parameterNumber: (button-1)*40, configurationValue: [hubId, (button-1)*40 + 1, 0, 0]).format() + cmds << zwave.configurationV1.configurationSet(parameterNumber: (button-1)*40 + 20, configurationValue: [hubId, (button-1)*40 + 21, 0, 0]).format() + } + cmds +} + +def configure() { + def cmds = configurationCmds() + log.debug("Sending configuration: $cmds") + return cmds +} diff --git a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy new file mode 100644 index 00000000000..6b283e908e2 --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy @@ -0,0 +1,283 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +metadata { + definition (name: "Aeon Multisensor 6", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Illuminance Measurement" + capability "Ultraviolet Index" + capability "Configuration" + capability "Sensor" + capability "Battery" + + attribute "tamper", "enum", ["detected", "clear"] + + fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" + } + + simulator { + status "no motion" : "command: 9881, payload: 00300300" + status "motion" : "command: 9881, payload: 003003FF" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) + ).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "humidity ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 5) + ).incomingMessage() + } + + for (int i in [0, 20, 89, 100, 200, 500, 1000]) { + status "illuminance ${i} lux": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 3) + ).incomingMessage() + } + + for (int i in [0, 5, 10, 15, 50, 99, 100]) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) + ).incomingMessage() + } + status "low battery alert": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: 255) + ).incomingMessage() + + status "wake up" : "command: 8407, payload: " + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "humidity", label:'${currentValue}% humidity', unit:"" + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main(["motion", "temperature", "humidity", "illuminance"]) + details(["motion", "temperature", "humidity", "illuminance", "battery"]) + } +} + +def updated() +{ + if (state.sec && !isConfigured()) { + // in case we miss the SCSR + response(configure()) + } +} + +def parse(String description) +{ + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "Parsed '${description}' to ${result.inspect()}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + + if (!isConfigured()) { + // we're still in the process of configuring a newly joined device + log.debug("late configure") + result += response(configure()) + } else { + result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + state.sec = 1 + log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + response(configure()) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + break + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "lux" + break + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + break + case 0x1B: + map.name = "ultravioletIndex" + map.value = cmd.scaledSensorValue.toInteger() + break + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + setConfigured() + motionEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + motionEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + result << motionEvent(0) + result << createEvent(name: "tamper", value: "clear", displayed: false) + break + case 3: + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved") + break + case 7: + result << motionEvent(1) + break + } + } else { + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def configure() { + // This sensor joins as a secure device if you double-click the button to include it + if (device.device.rawDescription =~ /98/ && !state.sec) { + log.debug "Multi 6 not sending configure until secure" + return [] + } + log.debug "Multi 6 configure()" + def request = [ + // send no-motion report 20 seconds after motion stops + zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20), + + // report every 8 minutes (threshold reports don't work on battery power) + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60), + + // report automatically on threshold change + zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1), + + zwave.batteryV1.batteryGet(), + zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C), + ] + commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] +} + +private setConfigured() { + updateDataValue("configured", "true") +} + +private isConfigured() { + getDataValue("configured") == "true" +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} diff --git a/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy new file mode 100644 index 00000000000..4124aa83529 --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy @@ -0,0 +1,263 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Multisensor Gen5", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Illuminance Measurement" + capability "Configuration" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A" + } + + simulator { + status "no motion" : "command: 9881, payload: 00300300" + status "motion" : "command: 9881, payload: 003003FF" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) + ).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "humidity ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 5) + ).incomingMessage() + } + + for (int i in [0, 20, 89, 100, 200, 500, 1000]) { + status "illuminance ${i} lux": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 3) + ).incomingMessage() + } + + for (int i in [0, 5, 10, 15, 50, 99, 100]) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) + ).incomingMessage() + } + status "low battery alert": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: 255) + ).incomingMessage() + + status "wake up" : "command: 8407, payload: " + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "humidity", label:'${currentValue}% humidity', unit:"" + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configureAfterSecure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configureAfterSecure", icon:"st.secondary.configure" + } + + main(["motion", "temperature", "humidity", "illuminance"]) + details(["motion", "temperature", "humidity", "illuminance", "battery", "configureAfterSecure"]) + } +} + +def parse(String description) +{ + def result = null + if (description == "updated") { + result = null + } else { + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "Parsed '${description}' to ${result.inspect()}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + + if (!isConfigured()) { + // we're still in the process of configuring a newly joined device + log.debug("not sending wakeUpNoMoreInformation yet") + result += response(configureAfterSecure()) + } else { + result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + // log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + // log.debug "Received SecurityCommandsSupportedReport" + response(configureAfterSecure()) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + break; + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "lux" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + break; + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + motionEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + motionEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationType == 7 && cmd.event == 7) { + motionEvent(cmd.notificationStatus) + } else { + createEvent(descriptionText: cmd.toString(), isStateChange: false) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def configureAfterSecure() { + // log.debug "configureAfterSecure()" + + def request = [ + // send temperature, humidity, and illuminance every 8 minutes + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 128|64|32), + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60), + + // send battery every 20 hours + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1), + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 20*60*60), + + // send no-motion report 60 seconds after motion stops + zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 60), + + // send binary sensor report instead of basic set for motion + zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2), + + // disable notification-style motion events + zwave.notificationV3.notificationSet(notificationType: 7, notificationStatus: 0), + + zwave.batteryV1.batteryGet(), + zwave.sensorBinaryV2.sensorBinaryGet(sensorType:0x0C) + ] + + setConfigured() + + secureSequence(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] +} + +def configure() { + // log.debug "configure()" + //["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet()) +} + +private setConfigured() { + device.updateDataValue("configured", "true") +} + +private isConfigured() { + device.getDataValue(["configured"]).toString() == "true" +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} + diff --git a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy new file mode 100644 index 00000000000..a00a9737b66 --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy @@ -0,0 +1,194 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Multisensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Configuration" + capability "Illuminance Measurement" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86" + } + + simulator { + // messages the device returns in response to commands it receives + status "motion (basic)" : "command: 2001, payload: FF" + status "no motion (basic)" : "command: 2001, payload: 00" + status "motion (binary)" : "command: 3003, payload: FF" + status "no motion (binary)" : "command: 3003, payload: 00" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "humidity ${i}%": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 5).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + for (int i = 200; i <= 1000; i += 200) { + status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "humidity", label:'${currentValue}% humidity', unit:"" + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main(["motion", "temperature", "humidity", "illuminance"]) + details(["motion", "temperature", "humidity", "illuminance", "battery", "configure"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) +{ + def result = [] + def cmd = zwave.parse(description, [0x31: 2, 0x30: 1, 0x84: 1]) + if (cmd) { + if( cmd.CMD == "8407" ) { result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) } + result << createEvent(zwaveEvent(cmd)) + } + log.debug "Parse returned ${result}" + return result +} + +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + [descriptionText: "${device.displayName} woke up", isStateChange: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + break; + case 3: + // luminance + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + map.name = "illuminance" + break; + case 5: + // humidity + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "%" + map.name = "humidity" + break; + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 + map.unit = "%" + map.displayed = false + map +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + def map = [:] + map.value = cmd.sensorValue ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected motion" + } + else { + map.descriptionText = "$device.displayName motion has stopped" + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + def map = [:] + map.value = cmd.value ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected motion" + } + else { + map.descriptionText = "$device.displayName motion has stopped" + } + map +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Catchall reached for cmd: ${cmd.toString()}}" + [:] +} + +def configure() { + delayBetween([ + // send binary sensor report instead of basic set for motion + zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(), + + // send no-motion report 15 seconds after motion stops + zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 15).format(), + + // send all data (temperature, humidity, illuminance & battery) periodically + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 225).format(), + + // set data reporting period to 5 minutes + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format() + ]) +} diff --git a/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy new file mode 100644 index 00000000000..e356fe62f57 --- /dev/null +++ b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy @@ -0,0 +1,148 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Outlet", namespace: "smartthings", author: "SmartThings") { + capability "Energy Meter" + capability "Actuator" + capability "Switch" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "reset" + + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86", outClusters: "0x82" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV2.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","energy","reset","refresh","configure"]) + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x20: 1, 0x32: 2]) + if (cmd) { + log.debug cmd + result = createEvent(zwaveEvent(cmd)) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv2.MeterReport cmd) { + if (cmd.scale == 0) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } + else { + [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + [ + name: "switch", value: cmd.value ? "on" : "off", type: "physical" + ] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + [ + name: "switch", value: cmd.value ? "on" : "off", type: "digital" + ] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet().format() + ]) +} + +def refresh() { + zwave.switchBinaryV1.switchBinaryGet().format() +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet().format() + ] +} + +def configure() { + delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 8).format(), // energy in kWh + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 0).format(), + zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format() + ]) +} diff --git a/devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy b/devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy new file mode 100644 index 00000000000..fbcbc4087ec --- /dev/null +++ b/devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy @@ -0,0 +1,254 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Aeon RGBW LED Bulb + * + * Author: SmartThings + * Date: 2015-7-12 + */ + +metadata { + definition (name: "Aeon LED Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + command "reset" + + fingerprint inClusters: "0x26,0x33,0x98" + fingerprint deviceId: "0x11", inClusters: "0x98,0x33" + fingerprint deviceId: "0x1102", inClusters: "0x98" + } + + simulator { + } + + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setColor" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false) { + state "colorTemperature", action:"setColorTemperature" + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } + + main(["switch"]) + details(["switch", "levelSliderControl", "rgbSelector", "reset", "colorTempControl", "refresh"]) +} + +def updated() { + response(refresh()) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(physicalgraph.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + response(command(zwave.switchMultilevelV1.switchMultilevelGet())) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof physicalgraph.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +def on() { + command(zwave.basicV1.basicSet(value: 0xFF)) +} + +def off() { + command(zwave.basicV1.basicSet(value: 0x00)) +} + +def setLevel(level) { + setLevel(level, 1) +} + +def setLevel(level, duration) { + if(level > 99) level = 99 + command(zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration)) +} + +def refresh() { + commands([ + zwave.switchMultilevelV1.switchMultilevelGet(), + ], 1000) +} + +def setSaturation(percent) { + log.debug "setSaturation($percent)" + setColor(saturation: percent) +} + +def setHue(value) { + log.debug "setHue($value)" + setColor(hue: value) +} + +def setColor(value) { + def result = [] + log.debug "setColor: ${value}" + if (value.hex) { + def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) + } else { + def hue = value.hue ?: device.currentValue("hue") + def saturation = value.saturation ?: device.currentValue("saturation") + if(hue == null) hue = 13 + if(saturation == null) saturation = 13 + def rgb = huesatToRGB(hue, saturation) + result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0) + } + + if(value.hue) sendEvent(name: "hue", value: value.hue) + if(value.hex) sendEvent(name: "color", value: value.hex) + if(value.switch) sendEvent(name: "switch", value: value.switch) + if(value.saturation) sendEvent(name: "saturation", value: value.saturation) + + commands(result) +} + +def setColorTemperature(percent) { + if(percent > 99) percent = 99 + int warmValue = percent * 255 / 99 + command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue))) +} + +def reset() { + log.debug "reset()" + sendEvent(name: "color", value: "#ffffff") + setColorTemperature(99) +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec != 0) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + float r = red / 255f + float g = green / 255f + float b = blue / 255f + float max = [r, g, b].max() + float delta = max - [r, g, b].min() + def hue = 13 + def saturation = 0 + if (max && delta) { + saturation = 100 * delta / max + if (r == max) { + hue = ((g - b) / delta) * 100 / 6 + } else if (g == max) { + hue = (2 + (b - r) / delta) * 100 / 6 + } else { + hue = (4 + (r - g) / delta) * 100 / 6 + } + } + [hue: hue, saturation: saturation, value: max * 100] +} + +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] + } +} diff --git a/devicetypes/smartthings/aeon-secure-smart-energy-switch-uk.src/aeon-secure-smart-energy-switch-uk.groovy b/devicetypes/smartthings/aeon-secure-smart-energy-switch-uk.src/aeon-secure-smart-energy-switch-uk.groovy new file mode 100644 index 00000000000..70cfee6563e --- /dev/null +++ b/devicetypes/smartthings/aeon-secure-smart-energy-switch-uk.src/aeon-secure-smart-energy-switch-uk.groovy @@ -0,0 +1,214 @@ +// This device file is based on work previous work done by "Mike '@jabbera'" + +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon Secure Smart Energy Switch UK", namespace: "smartthings", author: "jabbera") { + capability "Energy Meter" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Configuration" + + command "reset" + command "configureAfterSecure" + + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86,0x98", outClusters: "0x82" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power", "device.power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + standardTile("configureAfterSecure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configureAfterSecure", icon:"st.secondary.configure" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","power","energy","reset","configureAfterSecure","refresh"]) + } +} + +def parse(String description) { + def result = null + + if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x25: 1, 0x98: 1, 0x70: 1, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1, 0x72: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "Parsed '${description}' to ${result.inspect()}" + return result +} + +// Devices that support the Security command class can send messages in an encrypted form; +// they arrive wrapped in a SecurityMessageEncapsulation command and must be unencapsulated +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x32: 1, 0x25: 1, 0x98: 1, 0x70: 1, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1]) // can specify command class versions here like in zwave.parse + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { + def newEvent = null + if (cmd.scale == 0) { + newEvent = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + newEvent = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } else { + newEvent = [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } + + createEvent(newEvent) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + createEvent([ + name: "switch", value: cmd.value ? "on" : "off", type: "physical" + ]) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + createEvent([ + name: "switch", value: cmd.value ? "on" : "off", type: "digital" + ]) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "No handler for $cmd" + // Handles all Z-Wave commands we aren't interested in + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def on() { + secureSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + secureSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def poll() { + secureSequence([ + zwave.switchBinaryV1.switchBinaryGet(), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2) + ]) +} + +def refresh() { + secureSequence([ + zwave.switchBinaryV1.switchBinaryGet(), + zwave.meterV2.meterGet(scale: 0), + zwave.meterV2.meterGet(scale: 2) + ]) +} + +def reset() { + return secureSequence([ + zwave.meterV2.meterReset(), + zwave.meterV2.meterGet(scale: 0) + ]) +} + +def configureAfterSecure() { + log.debug "configureAfterSecure()" + + secureSequence([ + zwave.configurationV1.configurationSet(parameterNumber: 252, size: 1, scaledConfigurationValue: 0), // Enable/disable Configuration Locked (0 =disable, 1 = enable). + zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2), // Enable to send notifications to associated devices (Group 1) when the state of Micro Switch’s load changed (0=nothing, 1=hail CC, 2=basic CC report). + zwave.configurationV1.configurationSet(parameterNumber: 90, size: 1, scaledConfigurationValue: 1), // Enables/disables parameter 91 and 92 below (1=enabled, 0=disabled). + zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: 2), // The value here represents minimum change in wattage (in terms of wattage) for a REPORT to be sent (Valid values 0‐ 60000). + zwave.configurationV1.configurationSet(parameterNumber: 92, size: 1, scaledConfigurationValue: 5), // The value here represents minimum change in wattage percent (in terms of percentage) for a REPORT to be sent (Valid values 0‐100). + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4), // Which reports need to send in Report group 1 (See flags in table below). + // Disable a time interval to receive immediate updates of power change. + //zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300), // The time interval of sending Report group 1 (Valid values 0x01‐0xFFFFFFFF). + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8), // Which reports need to send in Report group 2 (See flags in table below). + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300), // The time interval of sending Report group 2 (Valid values 0x01‐0xFFFFFFFF). + zwave.configurationV1.configurationSet(parameterNumber: 252, size: 1, scaledConfigurationValue: 1), // Enable/disable Configuration Locked (0 =disable, 1 = enable). + + // Register for Group 1 + zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]), + // Register for Group 2 + zwave.associationV2.associationSet(groupingIdentifier:2, nodeId: [zwaveHubNodeId]) + ]) +} + +def configure() { + // Wait until after the secure exchange for this + log.debug "configure()" +} + +def updated() { + log.debug "updated()" + response(["delay 2000"] + configureAfterSecure() + refresh()) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} diff --git a/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy b/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy new file mode 100644 index 00000000000..34b73d6a212 --- /dev/null +++ b/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy @@ -0,0 +1,148 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Aeon Siren + * + * Author: SmartThings + * Date: 2014-07-15 + */ +metadata { + definition (name: "Aeon Siren", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Alarm" + capability "Switch" + + command "test" + + fingerprint deviceId: "0x1005", inClusters: "0x5E,0x98" + } + + simulator { + // reply messages + reply "9881002001FF,9881002002": "command: 9881, payload: 002003FF" + reply "988100200100,9881002002": "command: 9881, payload: 00200300" + reply "9881002001FF,delay 3000,988100200100,9881002002": "command: 9881, payload: 00200300" + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label:'off', action:'alarm.siren', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + standardTile("test", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"test", icon:"st.secondary.test" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + } + + preferences { + input "sound", "number", title: "Siren sound (1-5)", defaultValue: 1, required: true//, displayDuringSetup: true // don't display during setup until defaultValue is shown + input "volume", "number", title: "Volume (1-3)", defaultValue: 3, required: true//, displayDuringSetup: true + } + + main "alarm" + details(["alarm", "test", "off"]) + } +} + +def updated() { + if(!state.sound) state.sound = 1 + if(!state.volume) state.volume = 3 + + log.debug "settings: ${settings.inspect()}, state: ${state.inspect()}" + + Short sound = (settings.sound as Short) ?: 1 + Short volume = (settings.volume as Short) ?: 3 + + if (sound != state.sound || volume != state.volume) { + state.sound = sound + state.volume = volume + return response([ + secure(zwave.configurationV1.configurationSet(parameterNumber: 37, size: 2, configurationValue: [sound, volume])), + "delay 1000", + secure(zwave.basicV1.basicSet(value: 0x00)), + ]) + } +} + +def parse(String description) { + log.debug "parse($description)" + def result = null + def cmd = zwave.parse(description, [0x98: 1, 0x20: 1, 0x70: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parse returned ${result?.inspect()}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1]) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "rx $cmd" + [ + createEvent([name: "switch", value: cmd.value ? "on" : "off", displayed: false]), + createEvent([name: "alarm", value: cmd.value ? "both" : "off"]) + ] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def on() { + log.debug "sending on" + [ + secure(zwave.basicV1.basicSet(value: 0xFF)), + secure(zwave.basicV1.basicGet()) + ] +} + +def off() { + log.debug "sending off" + [ + secure(zwave.basicV1.basicSet(value: 0x00)), + secure(zwave.basicV1.basicGet()) + ] +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} + +def test() { + [ + secure(zwave.basicV1.basicSet(value: 0xFF)), + "delay 3000", + secure(zwave.basicV1.basicSet(value: 0x00)), + secure(zwave.basicV1.basicGet()) + ] +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} diff --git a/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy b/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy new file mode 100644 index 00000000000..cfd13305c78 --- /dev/null +++ b/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy @@ -0,0 +1,282 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Aeon SmartStrip", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Actuator" + capability "Sensor" + + command "reset" + + (1..4).each { n -> + attribute "switch$n", "enum", ["on", "off"] + attribute "power$n", "number" + attribute "energy$n", "number" + command "on$n" + command "off$n" + command "reset$n" + } + + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x70,0x85,0x72,0x86,0x60", outClusters: "0x82" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "switch1 on": "command: 600D, payload: 01 00 25 03 FF" + status "switch1 off": "command: 600D, payload: 01 00 25 03 00" + status "switch4 on": "command: 600D, payload: 04 00 25 03 FF" + status "switch4 off": "command: 600D, payload: 04 00 25 03 00" + status "power": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + status "energy": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + status "power1": "command: 600D, payload: 0100" + new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 30, precision: 3, meterType: 4, scale: 2, size: 4).format() + status "energy2": "command: 600D, payload: 0200" + new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: 200, precision: 3, meterType: 0, scale: 0, size: 4).format() + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power", "device.power", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + (1..4).each { n -> + standardTile("switch$n", "switch$n", canChangeIcon: true) { + state "on", label: '${name}', action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power$n", "power$n", decoration: "flat") { + state "default", label:'${currentValue} W' + } + valueTile("energy$n", "energy$n", decoration: "flat") { + state "default", label:'${currentValue} kWh' + } + } + + main(["switch", "power", "energy", "switch1", "switch2", "switch3", "switch4"]) + details(["switch","power","energy", + "switch1","power1","energy1", + "switch2","power2","energy2", + "switch3","power3","energy3", + "switch4","power4","energy4", + "refresh","reset"]) + } +} + + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x60: 3, 0x32: 3, 0x25: 1, 0x20: 1]) + if (cmd) { + result = zwaveEvent(cmd, null) + } + } + log.debug "parsed '${description}' to ${result.inspect()}" + result +} + +def endpointEvent(endpoint, map) { + if (endpoint) { + map.name = map.name + endpoint.toString() + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + if (encapsulatedCommand.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + Integer endpoint = cmd.sourceEndPoint + if (endpoint > 2) { + zwaveEvent(encapsulatedCommand, endpoint - 2) + } else if (endpoint == 0) { + zwaveEvent(encapsulatedCommand, 0) + } else { + log.debug("Ignoring metered outlet $endpoint msg: $encapsulatedCommand") + [] + } + } else { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint) { + def map = [name: "switch", type: "physical", value: (cmd.value ? "on" : "off")] + def events = [endpointEvent(endpoint, map)] + def cmds = [] + if (endpoint) { + cmds += delayBetween([2,0].collect { s -> encap(zwave.meterV3.meterGet(scale: s), endpoint) }, 1000) + if(endpoint < 4) cmds += ["delay 1500", encap(zwave.basicV1.basicGet(), endpoint + 1)] + } else if (events[0].isStateChange) { + events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) } + cmds << "delay 3000" + cmds += delayBetween((0..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) }, 800) + } + if(cmds) events << response(cmds) + events +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint) { + def map = [name: "switch", value: (cmd.value ? "on" : "off")] + def events = [endpointEvent(endpoint, map)] + def cmds = [] + if (!endpoint && events[0].isStateChange) { + events += (1..4).collect { ep -> endpointEvent(ep, map.clone()) } + cmds << "delay 3000" + cmds += delayBetween((1..4).collect { ep -> encap(zwave.meterV3.meterGet(scale: 2), ep) }) + } + if(cmds) events << response(cmds) + events +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep) { + def event = [:] + def cmds = [] + if (cmd.scale < 2) { + def val = Math.round(cmd.scaledMeterValue*100)/100.0 + event = endpointEvent(ep, [name: "energy", value: val, unit: ["kWh", "kVAh"][cmd.scale]]) + } else { + event = endpointEvent(ep, [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"]) + } + if (!ep && event.isStateChange && event.name == "energy") { + // Total strip energy consumption changed, check individual outlets + (1..4).each { endpoint -> + cmds << encap(zwave.meterV2.meterGet(scale: 0), endpoint) + cmds << "delay 400" + } + } + cmds ? [event, response(cmds)] : event +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep) { + updateDataValue("MSR", String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)) + null +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep) { + log.debug "${device.displayName}: Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +def onOffCmd(value, endpoint = null) { + [ + encap(zwave.basicV1.basicSet(value: value), endpoint), + "delay 500", + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint), + "delay 3000", + encap(zwave.meterV3.meterGet(scale: 2), endpoint) + ] +} + +def on() { onOffCmd(0xFF) } +def off() { onOffCmd(0x0) } + +def on1() { onOffCmd(0xFF, 1) } +def on2() { onOffCmd(0xFF, 2) } +def on3() { onOffCmd(0xFF, 3) } +def on4() { onOffCmd(0xFF, 4) } + +def off1() { onOffCmd(0, 1) } +def off2() { onOffCmd(0, 2) } +def off3() { onOffCmd(0, 3) } +def off4() { onOffCmd(0, 4) } + +def refresh() { + delayBetween([ + zwave.basicV1.basicGet().format(), + zwave.meterV3.meterGet(scale: 0).format(), + zwave.meterV3.meterGet(scale: 2).format(), + encap(zwave.basicV1.basicGet(), 1) // further gets are sent from the basic report handler + ]) +} + +def resetCmd(endpoint = null) { + delayBetween([ + encap(zwave.meterV2.meterReset(), endpoint), + encap(zwave.meterV2.meterGet(scale: 0), endpoint) + ]) +} + +def reset() { + delayBetween([resetCmd(null), reset1(), reset2(), reset3(), reset4()]) +} + +def reset1() { resetCmd(1) } +def reset2() { resetCmd(2) } +def reset3() { resetCmd(3) } +def reset4() { resetCmd(4) } + +def configure() { + def cmds = [ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, configurationValue: [0, 0, 0, 1]).format(), + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, configurationValue: [0, 0, 0x79, 0]).format(), + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 90).format(), + ] + [5, 8, 9, 10, 11].each { p -> + cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 2, scaledConfigurationValue: 5).format() + } + [12, 15, 16, 17, 18].each { p -> + cmds << zwave.configurationV1.configurationSet(parameterNumber: p, size: 1, scaledConfigurationValue: 50).format() + } + cmds += [ + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 15*60).format(), + zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [1]).format(), + ] + delayBetween(cmds) + "delay 5000" + refresh() +} + +private encap(cmd, endpoint) { + if (endpoint) { + if (cmd.commandClassId == 0x32) { + // Metered outlets are numbered differently than switches + if (endpoint < 0x80) { + endpoint += 2 + } else { + endpoint = ((endpoint & 0x7F) << 2) | 0x80 + } + } + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} diff --git a/devicetypes/smartthings/arduino-thingshield.src/arduino-thingshield.groovy b/devicetypes/smartthings/arduino-thingshield.src/arduino-thingshield.groovy new file mode 100644 index 00000000000..4208212397d --- /dev/null +++ b/devicetypes/smartthings/arduino-thingshield.src/arduino-thingshield.groovy @@ -0,0 +1,48 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Arduino ThingShield", namespace: "smartthings", author: "SmartThings") { + + fingerprint profileId: "0104", deviceId: "0138", inClusters: "0000" + } + + // Simulator metadata + simulator { + // status messages + status "ping": "catchall: 0104 0000 01 01 0040 00 6A67 00 00 0000 0A 00 0A70696E67" + status "hello": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A48656c6c6f20576f726c6421" + } + + // UI tile definitions + tiles { + standardTile("shield", "device.shield", width: 2, height: 2) { + state "default", icon:"st.shields.shields.arduino", backgroundColor:"#ffffff" + } + + main "shield" + details "shield" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def value = zigbee.parse(description)?.text + def name = value && value != "ping" ? "response" : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +// Commands to device +// TBD diff --git a/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy b/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy new file mode 100644 index 00000000000..348239c402a --- /dev/null +++ b/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy @@ -0,0 +1,143 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * CentraLite Dimmer + * + * Author: SmartThings + * Date: 2013-12-04 + */ +metadata { + definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Configuration" + capability "Refresh" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("power", "device.power", decoration: "flat") { + state "power", label:'${currentValue} W' + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch","power","refresh","levelSliderControl"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "Parse description $description" + def name = null + def value = null + if (description?.startsWith("catchall:")) { + def msg = zigbee.parse(description) + log.trace msg + log.trace "data: $msg.data" + } else if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + log.debug "Read attr: $description" + if (descMap.cluster == "0006" && descMap.attrId == "0000") { + name = "switch" + value = descMap.value.endsWith("01") ? "on" : "off" + } else { + def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim() + name = "power" + // assume 16 bit signed for encoding and power divisor is 10 + value = Integer.parseInt(reportValue, 16) / 10 + } + } else if (description?.startsWith("on/off:")) { + log.debug "Switch command" + name = "switch" + value = description?.endsWith(" 1") ? "on" : "off" + } + + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +// Commands to device +def on() { + 'zcl on-off on' +} + +def off() { + 'zcl on-off off' +} + +def setLevel(value) { + log.trace "setLevel($value)" + sendEvent(name: "level", value: value) + def level = hexString(Math.round(value * 255/100)) + def cmd = "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 2000}" + log.debug cmd + cmd +} + +def meter() { + "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" +} + +def refresh() { + "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" +} + +def configure() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}" + ] +} + +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/centralite-thermostat.src/centralite-thermostat.groovy b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy new file mode 100644 index 00000000000..c9d87d8bbf8 --- /dev/null +++ b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy @@ -0,0 +1,280 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * CentraLite Thermostat + * + * Author: SmartThings + * Date: 2013-12-02 + */ +metadata { + definition (name: "CentraLite Thermostat", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Temperature Measurement" + capability "Thermostat" + capability "Configuration" + capability "Refresh" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0201,0202,0204,0B05", outClusters: "000A, 0019" + } + + // simulator metadata + simulator { } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"thermostat.setThermostatMode" + state "cool", label:'${name}', action:"thermostat.setThermostatMode" + state "heat", label:'${name}', action:"thermostat.setThermostatMode" + state "emergencyHeat", label:'${name}', action:"thermostat.setThermostatMode" + } + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode" + state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode" + } + controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" + } + controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + main "temperature" + details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "configure"]) + } +} + + +// parse events into attributes +def parse(String description) { + log.debug "Parse description $description" + def map = [:] + if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + if (descMap.cluster == "0201" && descMap.attrId == "0000") { + log.debug "TEMP" + map.name = "temperature" + map.value = getTemperature(descMap.value) + } else if (descMap.cluster == "0201" && descMap.attrId == "0011") { + log.debug "COOLING SETPOINT" + map.name = "coolingSetpoint" + map.value = getTemperature(descMap.value) + } else if (descMap.cluster == "0201" && descMap.attrId == "0012") { + log.debug "HEATING SETPOINT" + map.name = "heatingSetpoint" + map.value = getTemperature(descMap.value) + } else if (descMap.cluster == "0201" && descMap.attrId == "001c") { + log.debug "MODE" + map.name = "thermostatMode" + map.value = getModeMap()[descMap.value] + } else if (descMap.cluster == "0202" && descMap.attrId == "0000") { + log.debug "FAN MODE" + map.name = "thermostatFanMode" + map.value = getFanModeMap()[descMap.value] + } + } + + def result = null + if (map) { + result = createEvent(map) + } + log.debug "Parse returned $map" + return result +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def getModeMap() { [ + "00":"off", + "03":"cool", + "04":"heat", + "05":"emergencyHeat" +]} + +def getFanModeMap() { [ + "04":"fanOn", + "05":"fanAuto" +]} + +def refresh() +{ + log.debug "refresh called" + [ + "st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x11", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 0x202 0" + ] +} +// Leaving out for now as its killing the batteries. +//def poll() { +// log.debug "Executing 'poll'" +// refresh() +//} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16) / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +def setHeatingSetpoint(degrees) { + def temperatureScale = getTemperatureScale() + + def degreesInteger = degrees as Integer + log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})" + sendEvent("name":"heatingSetpoint", "value":degreesInteger) + + def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}" +} + +def setCoolingSetpoint(degrees) { + def degreesInteger = degrees as Integer + log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})" + sendEvent("name":"coolingSetpoint", "value":degreesInteger) + def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}" +} + +def modes() { + ["off", "heat", "cool"] +} + +def setThermostatMode() { + log.debug "switching thermostatMode" + def currentMode = device.currentState("thermostatMode")?.value + def modeOrder = modes() + def index = modeOrder.indexOf(currentMode) + def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0] + log.debug "switching mode from $currentMode to $next" + "$next"() +} + +def setThermostatFanMode() { + log.debug "Switching fan mode" + def currentFanMode = device.currentState("thermostatFanMode")?.value + log.debug "switching fan from current mode: $currentFanMode" + def returnCommand + + switch (currentFanMode) { + case "fanAuto": + returnCommand = fanOn() + break + case "fanOn": + returnCommand = fanAuto() + break + } + if(!currentFanMode) { returnCommand = fanAuto() } + returnCommand +} + +def setThermostatMode(String value) { + log.debug "setThermostatMode({$value})" + "$value"() +} + +def setThermostatFanMode(String value) { + log.debug "setThermostatFanMode({$value})" + "$value"() +} + +def off() { + log.debug "off" + sendEvent("name":"thermostatMode", "value":"off") + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}" +} + +def cool() { + log.debug "cool" + sendEvent("name":"thermostatMode", "value":"cool") + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {03}" +} + +def heat() { + log.debug "heat" + sendEvent("name":"thermostatMode", "value":"heat") + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}" +} + +def emergencyHeat() { + log.debug "emergencyHeat" + sendEvent("name":"thermostatMode", "value":"emergency heat") + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {05}" +} + +def on() { + fanOn() +} + +def fanOn() { + log.debug "fanOn" + sendEvent("name":"thermostatFanMode", "value":"fanOn") + "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}" +} + +def auto() { + fanAuto() +} + +def fanAuto() { + log.debug "fanAuto" + sendEvent("name":"thermostatFanMode", "value":"fanAuto") + "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}" +} + +def configure() { + + log.debug "binding to Thermostat and Fan Control cluster" + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}" + ] +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} diff --git a/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy b/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy new file mode 100644 index 00000000000..a669426a66a --- /dev/null +++ b/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy @@ -0,0 +1,184 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Cooper RF9500", namespace: "smartthings", author: "juano23@gmail.com") { + capability "Switch" + capability "Switch Level" + capability "Button" + capability "Actuator" + + //fingerprint deviceId: "0x1200", inClusters: "0x77 0x86 0x75 0x73 0x85 0x72 0xEF", outClusters: "0x26" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.Home.home30", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.Home.home30", backgroundColor: "#79b821" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" + } + main "switch" + details(["switch", "refresh", "level", "levelSliderControl"]) + } +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x26: 1, 0x2B: 1, 0x80: 1, 0x84: 1]) + if(cmd) results += zwaveEvent(cmd) + if(!results) results = [ descriptionText: cmd, displayed: false ] + } + log.debug("Parsed '$description' to $results") + return results +} + +def on() { + sendEvent(name: "switch", value: "on") +} + +def off() { + sendEvent(name: "switch", value: "off") +} + +def levelup() { + def curlevel = device.currentValue('level') as Integer + if (curlevel <= 90) + setLevel(curlevel + 10); +} + +def leveldown() { + def curlevel = device.currentValue('level') as Integer + if (curlevel >= 10) + setLevel(curlevel - 10) +} + +def setLevel(value) { + log.trace "setLevel($value)" + sendEvent(name: "level", value: value) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)] + + results += configurationCmds().collect{ response(it) } + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + + return results +} + +// A zwave command for a button press was received convert to button number +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd) { + [ descriptionText: "startlevel $cmd"] + log.info "startlevel $cmd" + if (cmd.upDown == true) { + Integer buttonid = 2 + leveldown() + checkbuttonEvent(buttonid) + } else if (cmd.upDown == false) { + Integer buttonid = 3 + levelup() + checkbuttonEvent(buttonid) + } +} + +// The controller likes to repeat the command... ignore repeats +def checkbuttonEvent(buttonid){ + + if (state.lastScene == buttonid && (state.repeatCount < 4) && (now() - state.repeatStart < 2000)) { + log.debug "Button ${buttonid} repeat ${state.repeatCount}x ${now()}" + state.repeatCount = state.repeatCount + 1 + createEvent([:]) + } + else { + // If the button was really pressed, store the new scene and handle the button press + state.lastScene = buttonid + state.lastLevel = 0 + state.repeatCount = 0 + state.repeatStart = now() + + buttonEvent(buttonid) + } +} + +// Handle a button being pressed +def buttonEvent(button) { + button = button as Integer + log.trace "Button $button pressed" + def result = [] + if (button == 1) { + def mystate = device.currentValue('switch'); + if (mystate == "on") + off() + else + on() + } + updateState("currentButton", "$button") + // update the device state, recording the button press + result << createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicGet cmd) { + [ descriptionText: "$cmd"] + if(1){ + Integer buttonid = 1 + checkbuttonEvent(buttonid) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + [ descriptionText: "$cmd"] + if(1){ + Integer buttonid = 1 + log.info "button $buttonid pressed" + checkbuttonEvent(buttonid) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + createEvent([:]) +} + + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [ descriptionText: "$cmd"] +} + +// Update State +def updateState(String name, String value) { + state[name] = value + device.updateDataValue(name, value) +} diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy new file mode 100644 index 00000000000..9107f12d0b1 --- /dev/null +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -0,0 +1,206 @@ +/** + * Cree Bulb + * + * Copyright 2014 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: "Cree Bulb", namespace: "smartthings", author: "SmartThings") { + + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Switch Level" + + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + + main(["switch"]) + details(["switch", "level", "levelSliderControl", "refresh"]) + } +} + +// Parse incoming device messages to generate events + +def parse(String description) { + log.trace description + if (description?.startsWith("catchall:")) { + def msg = zigbee.parse(description) + log.trace msg + log.trace "data: $msg.data" + + if(description?.endsWith("0100") ||description?.endsWith("1001")) + { + def result = createEvent(name: "switch", value: "on") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + + if(description?.endsWith("0000") || description?.endsWith("1000")) + { + def result = createEvent(name: "switch", value: "off") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + } + + + if (description?.startsWith("read attr")) { + + log.debug description[-2..-1] + def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) + + sendEvent( name: "level", value: i ) + } + + +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + } + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + +} + +def refresh() { + // Schedule poll every 1 min + //schedule("0 */1 * * * ?", poll) + //poll() + + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0" + ] +} + +def setLevel(value) { + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: value) + def level = hex(value * 255/100) + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" + + //log.debug cmds + cmds +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting and Bindings." + def configCmds = [ + + //Switch Reporting + "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", + + //Level Control Reporting + "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def uninstalled() { + + log.debug "uninstalled()" + + response("zcl rftd") + +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + + + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy new file mode 100644 index 00000000000..1737fed37e4 --- /dev/null +++ b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy @@ -0,0 +1,604 @@ +metadata { + // Automatically generated. Make future change here. + definition (name: "CT100 Thermostat", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Thermostat" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Sensor" + + attribute "thermostatFanState", "string" + + command "switchMode" + command "switchFanMode" + command "quickSetCool" + command "quickSetHeat" + + fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60" + } + + // simulator metadata + simulator { + status "off" : "command: 4003, payload: 00" + status "heat" : "command: 4003, payload: 01" + status "cool" : "command: 4003, payload: 02" + status "auto" : "command: 4003, payload: 03" + status "emergencyHeat" : "command: 4003, payload: 04" + + status "fanAuto" : "command: 4403, payload: 00" + status "fanOn" : "command: 4403, payload: 01" + status "fanCirculate" : "command: 4403, payload: 06" + + status "heat 60" : "command: 4303, payload: 01 09 3C" + status "heat 72" : "command: 4303, payload: 01 09 48" + + status "cool 76" : "command: 4303, payload: 02 09 4C" + status "cool 80" : "command: 4303, payload: 02 09 50" + + status "temp 58" : "command: 3105, payload: 01 2A 02 44" + status "temp 62" : "command: 3105, payload: 01 2A 02 6C" + status "temp 78" : "command: 3105, payload: 01 2A 03 0C" + status "temp 86" : "command: 3105, payload: 01 2A 03 34" + + status "idle" : "command: 4203, payload: 00" + status "heating" : "command: 4203, payload: 01" + status "cooling" : "command: 4203, payload: 02" + + // reply messages + reply "2502": "command: 2503, payload: FF" + } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + ) + } + standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"switchMode", nextState:"to_heat" + state "heat", label:'${name}', action:"switchMode", nextState:"to_cool" + state "cool", label:'${name}', action:"switchMode", nextState:"..." + state "auto", label:'${name}', action:"switchMode", nextState:"..." + state "emergency heat", label:'${name}', action:"switchMode", nextState:"..." + state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool" + state "to_cool", label: "cool", action:"switchMode", nextState:"..." + state "...", label: "...", action:"off", nextState:"off" + } + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"switchFanMode" + state "fanOn", label:'${name}', action:"switchFanMode" + state "fanCirculate", label:'${name}', action:"switchFanMode" + } + controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff" + } + controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff" + } + valueTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat") { + state "humidity", label:'${currentValue}% humidity', unit:"" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "temperature" + details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "humidity", "battery"]) + } +} + +def parse(String description) +{ + def result = [] + if (description == "updated") { + } else { + def zwcmd = zwave.parse(description, [0x42:2, 0x43:2, 0x31: 2, 0x60: 3]) + if (zwcmd) { + result += zwaveEvent(zwcmd) + } else { + log.debug "$device.displayName couldn't parse $description" + } + } + if (!result) { + return null + } + if (result.size() == 1 && (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000)) { + result << response(zwave.batteryV1.batteryGet().format()) + } + log.debug "$device.displayName parsed '$description' to $result" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def result = null + def encapsulatedCommand = cmd.encapsulatedCommand([0x42:2, 0x43:2, 0x31: 2]) + log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") + if (encapsulatedCommand) { + result = zwaveEvent(encapsulatedCommand) + if (cmd.sourceEndPoint == 1) { // indicates a response to refresh() vs an unrequested update + def event = ([] + result)[0] // in case zwaveEvent returns a list + def resp = nextRefreshQuery(event?.name) + if (resp) { + log.debug("sending next refresh query: $resp") + result = [] + result + response(["delay 200", resp]) + } + } + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) +{ + def cmdScale = cmd.scale == 1 ? "F" : "C" + def temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) + def unit = getTemperatureScale() + def map1 = [ value: temp, unit: unit, displayed: false ] + switch (cmd.setpointType) { + case 1: + map1.name = "heatingSetpoint" + break; + case 2: + map1.name = "coolingSetpoint" + break; + default: + log.debug "unknown setpointType $cmd.setpointType" + return + } + + // So we can respond with same format + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + + def mode = device.latestValue("thermostatMode") + if (mode && map1.name.startsWith(mode) || (mode == "emergency heat" && map1.name == "heatingSetpoint")) { + def map2 = [ name: "thermostatSetpoint", value: temp, unit: unit ] + [ createEvent(map1), createEvent(map2) ] + } else { + createEvent(map1) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) +{ + def map = [:] + if (cmd.sensorType == 1) { + map.name = "temperature" + map.unit = getTemperatureScale() + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + } else if (cmd.sensorType == 5) { + map.name = "humidity" + map.unit = "%" + map.value = cmd.scaledSensorValue + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) +{ + def map = [name: "thermostatOperatingState" ] + switch (cmd.operatingState) { + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: + map.value = "idle" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_HEATING: + map.value = "heating" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_COOLING: + map.value = "cooling" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY: + map.value = "fan only" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT: + map.value = "pending heat" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL: + map.value = "pending cool" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER: + map.value = "vent economizer" + break + } + def result = createEvent(map) + if (result.isStateChange && device.latestValue("thermostatMode") == "auto" && (result.value == "heating" || result.value == "cooling")) { + def thermostatSetpoint = device.latestValue("${result.value}Setpoint") + result = [result, createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale())] + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { + def map = [name: "thermostatFanState", unit: ""] + switch (cmd.fanOperatingState) { + case 0: + map.value = "idle" + break + case 1: + map.value = "running" + break + case 2: + map.value = "running high" + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def map = [name: "thermostatMode"] + def thermostatSetpoint = null + switch (cmd.mode) { + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: + map.value = "off" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: + map.value = "heat" + thermostatSetpoint = device.latestValue("heatingSetpoint") + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: + map.value = "emergency heat" + thermostatSetpoint = device.latestValue("heatingSetpoint") + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: + map.value = "cool" + thermostatSetpoint = device.latestValue("coolingSetpoint") + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: + map.value = "auto" + def temp = device.latestValue("temperature") + def heatingSetpoint = device.latestValue("heatingSetpoint") + def coolingSetpoint = device.latestValue("coolingSetpoint") + if (temp && heatingSetpoint && coolingSetpoint) { + if (temp < (heatingSetpoint + coolingSetpoint) / 2.0) { + thermostatSetpoint = heatingSetpoint + } else { + thermostatSetpoint = coolingSetpoint + } + } + break + } + state.lastTriedMode = map.value + if (thermostatSetpoint) { + [ createEvent(map), createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) ] + } else { + createEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { + def map = [name: "thermostatFanMode", displayed: false] + switch (cmd.fanMode) { + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: + map.value = "fanAuto" + break + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: + map.value = "fanOn" + break + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: + map.value = "fanCirculate" + break + } + state.lastTriedFanMode = map.value + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def supportedModes = "" + if(cmd.off) { supportedModes += "off " } + if(cmd.heat) { supportedModes += "heat " } + if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " } + if(cmd.cool) { supportedModes += "cool " } + if(cmd.auto) { supportedModes += "auto " } + + state.supportedModes = supportedModes + [ createEvent(name:"supportedModes", value: supportedModes, displayed: false), + response(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet()) ] +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { + def supportedFanModes = "" + if(cmd.auto) { supportedFanModes += "fanAuto " } + if(cmd.low) { supportedFanModes += "fanOn " } + if(cmd.circulation) { supportedFanModes += "fanCirculate " } + + state.supportedFanModes = supportedFanModes + [ createEvent(name:"supportedFanModes", value: supportedModes, displayed: false), + response(refresh()) ] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "Zwave event received: $cmd" +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unexpected zwave command $cmd" +} + +def refresh() { + // Use encapsulation to differentiate refresh cmds from what the thermostat sends proactively on change + def cmd = zwave.sensorMultilevelV2.sensorMultilevelGet() + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format() +} + +def nextRefreshQuery(name) { + def cmd = null + switch (name) { + case "temperature": + cmd = zwave.thermostatModeV2.thermostatModeGet() + break + case "thermostatMode": + cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) + break + case "heatingSetpoint": + cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) + break + case "coolingSetpoint": + cmd = zwave.thermostatFanModeV3.thermostatFanModeGet() + break + case "thermostatFanMode": + cmd = zwave.thermostatOperatingStateV2.thermostatOperatingStateGet() + break + case "thermostatOperatingState": + // get humidity, multilevel sensor get to endpoint 2 + cmd = zwave.sensorMultilevelV2.sensorMultilevelGet() + return zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:2).encapsulate(cmd).format() + default: return null + } + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format() +} + +def quickSetHeat(degrees) { + setHeatingSetpoint(degrees, 1000) +} + +def setHeatingSetpoint(degrees, delay = 30000) { + setHeatingSetpoint(degrees.toDouble(), delay) +} + +def setHeatingSetpoint(Double degrees, Integer delay = 30000) { + log.trace "setHeatingSetpoint($degrees, $delay)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + + def convertedDegrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } else { + convertedDegrees = degrees + } + + delayBetween([ + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format() + ], delay) +} + +def quickSetCool(degrees) { + setCoolingSetpoint(degrees, 1000) +} + +def setCoolingSetpoint(degrees, delay = 30000) { + setCoolingSetpoint(degrees.toDouble(), delay) +} + +def setCoolingSetpoint(Double degrees, Integer delay = 30000) { + log.trace "setCoolingSetpoint($degrees, $delay)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + + def convertedDegrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } else { + convertedDegrees = degrees + } + + delayBetween([ + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format() + ], delay) +} + +def configure() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSupportedGet().format(), + ], 2300) +} + +def modes() { + ["off", "heat", "cool", "auto", "emergency heat"] +} + +def switchMode() { + def currentMode = device.currentState("thermostatMode")?.value + def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" + def supportedModes = getDataByName("supportedModes") + def modeOrder = modes() + def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } + def nextMode = next(lastTriedMode) + if (supportedModes?.contains(currentMode)) { + while (!supportedModes.contains(nextMode) && nextMode != "off") { + nextMode = next(nextMode) + } + } + state.lastTriedMode = nextMode + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], 1000) +} + +def switchToMode(nextMode) { + def supportedModes = getDataByName("supportedModes") + if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" + if (nextMode in modes()) { + state.lastTriedMode = nextMode + "$nextMode"() + } else { + log.debug("no mode method '$nextMode'") + } +} + +def switchFanMode() { + def currentMode = device.currentState("thermostatFanMode")?.value + def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off" + def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn" + def modeOrder = ["fanAuto", "fanCirculate", "fanOn"] + def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } + def nextMode = next(lastTriedMode) + while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") { + nextMode = next(nextMode) + } + switchToFanMode(nextMode) +} + +def switchToFanMode(nextMode) { + def supportedFanModes = getDataByName("supportedFanModes") + if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" + + def returnCommand + if (nextMode == "fanAuto") { + returnCommand = fanAuto() + } else if (nextMode == "fanOn") { + returnCommand = fanOn() + } else if (nextMode == "fanCirculate") { + returnCommand = fanCirculate() + } else { + log.debug("no fan mode '$nextMode'") + } + if(returnCommand) state.lastTriedFanMode = nextMode + returnCommand +} + +def getDataByName(String name) { + state[name] ?: device.getDataValue(name) +} + +def getModeMap() { [ + "off": 0, + "heat": 1, + "cool": 2, + "auto": 3, + "emergency heat": 4 +]} + +def setThermostatMode(String value) { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def getFanModeMap() { [ + "auto": 0, + "on": 1, + "circulate": 6 +]} + +def setThermostatFanMode(String value) { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +def off() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def heat() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def emergencyHeat() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def cool() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def auto() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def fanOn() { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +def fanAuto() { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +def fanCirculate() { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +private getStandardDelay() { + 1000 +} + diff --git a/devicetypes/smartthings/danalock.src/danalock.groovy b/devicetypes/smartthings/danalock.src/danalock.groovy new file mode 100644 index 00000000000..0cbc3306024 --- /dev/null +++ b/devicetypes/smartthings/danalock.src/danalock.groovy @@ -0,0 +1,244 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition(name: "Danalock", namespace: "smartthings", author: "SmartThings") { + capability "Lock" + capability "Battery" + capability "Polling" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + fingerprint deviceId: '0x4002', inClusters: '0x72,0x80,0x86,0x98' + } + + simulator { + status "locked": "command: 9881, payload: 00 62 03 FF 00 07 FE FE" + status "unlocked": "command: 9881, payload: 00 62 03 00 FF 06 FE FE" + + reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 07 FE FE" + reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 06 FE FE" + } + + tiles { + standardTile("toggle", "device.lock", width: 2, height: 2) { + state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking" + state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" + state "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" + state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" + state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + } + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking" + } + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "toggle" + details(["toggle", "lock", "unlock", "battery", "refresh"]) + } +} + +import physicalgraph.zwave.commands.doorlockv1.* + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else { + def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x8A: 1 ]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "\"$description\" parsed to ${result.inspect()}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x8A: 1, 0x85: 2, 0x98: 1]) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() + if (cmd.commandClassControl) { + state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() + } + log.debug "Security command classes: $state.sec" + createEvent(name:"secureInclusion", value:"success", descriptionText:"$device.displayName is securely included") +} + +def zwaveEvent(DoorLockOperationReport cmd) { + def result = [] + def map = [ name: "lock" ] + if (cmd.doorLockMode == 0xFF) { + map.value = "locked" + } else if (cmd.doorLockMode >= 0x40) { + map.value = "unknown" + } else if (cmd.doorLockMode & 1) { + map.value = "unlocked with timeout" + } else { + map.value = "unlocked" + } + result ? [createEvent(map), *result] : createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet cmd) { + def result = [] + def now = new Date().toCalendar() + if(location.timeZone) now.timeZone = location.timeZone + result << createEvent(descriptionText: "$device.displayName requested time update", displayed: false) + result << response(secure(zwave.timeV1.timeReport( + hourLocalTime: now.get(Calendar.HOUR_OF_DAY), + minuteLocalTime: now.get(Calendar.MINUTE), + secondLocalTime: now.get(Calendar.SECOND))) + ) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery" + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + def msg = cmd.status == 0 ? "try again later" : + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: false, descriptionText: "$device.displayName is busy, $msg") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def lockAndCheck(doorLockMode) { + secureSequence([ + zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), + zwave.doorLockV1.doorLockOperationGet() + ], 4200) +} + +def lock() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED) +} + +def unlock() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED) +} + +def unlockwtimeout() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT) +} + +def refresh() { + def cmds = [secure(zwave.doorLockV1.doorLockOperationGet())] + if (secondsPast(state.lastbatt, 60)) { + cmds << "delay 8000" + cmds << secure(zwave.batteryV1.batteryGet()) + } + log.debug "refresh sending ${cmds.inspect()}" + cmds +} + +def poll() { + def cmds = [] + // Only check lock state if it changed recently or we haven't had an update in an hour + def latest = device.currentState("lock")?.date?.time + if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 67 * 60)) { + cmds << secure(zwave.doorLockV1.doorLockOperationGet()) + state.lastPoll = (new Date()).time + } else if (secondsPast(state.lastbatt, 53 * 3600)) { + cmds << secure(zwave.batteryV1.batteryGet()) + } + if(cmds) cmds << "delay 6000" + log.debug "poll is sending ${cmds.inspect()}, state: ${state.inspect()}" + sendEvent(descriptionText: "poll sent ${cmds ?: 'nothing'}", isStateChange: false) // workaround to keep polling from being shut off + cmds ?: null +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=4200) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (new Date().time - timestamp) > (seconds * 1000) +} diff --git a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy new file mode 100644 index 00000000000..c77545f868c --- /dev/null +++ b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy @@ -0,0 +1,222 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Dimmer Switch", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Indicator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + + fingerprint inClusters: "0x26" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("indicator", "device.indicatorStatus", inactiveLabel: false, decoration: "flat") { + state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" + state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" + state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main(["switch"]) + details(["switch", "refresh", "indicator", "levelSliderControl"]) + } +} + +def parse(String description) { + def item1 = [ + canBeCurrentState: false, + linkText: getLinkText(device), + isStateChange: false, + displayed: false, + descriptionText: description, + value: description + ] + def result + def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1]) + if (cmd) { + result = createEvent(cmd, item1) + } + else { + item1.displayed = displayed(description, item1.isStateChange) + result = [item1] + } + log.debug "Parse returned ${result?.descriptionText}" + result +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) { + [] +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) { + [response(zwave.basicV1.basicGet())] +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + result[0].descriptionText = "${item1.linkText} is ${item1.value}" + result[0].handlerName = cmd.value ? "statusOn" : "statusOff" + for (int i = 0; i < result.size(); i++) { + result[i].type = "digital" + } + result +} + +def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { + def result = [item1] + + item1.name = "switch" + item1.value = cmd.value ? "on" : "off" + item1.handlerName = item1.value + item1.descriptionText = "${item1.linkText} was turned ${item1.value}" + item1.canBeCurrentState = true + item1.isStateChange = isStateChange(device, item1.name, item1.value) + item1.displayed = item1.isStateChange + + if (cmd.value >= 5) { + def item2 = new LinkedHashMap(item1) + item2.name = "level" + item2.value = cmd.value as String + item2.unit = "%" + item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" + item2.canBeCurrentState = true + item2.isStateChange = isStateChange(device, item2.name, item2.value) + item2.displayed = false + result << item2 + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + [name: "indicatorStatus", value: value, display: false] +} + +def createEvent(physicalgraph.zwave.Command cmd, Map map) { + // Handles any Z-Wave commands we aren't interested in + log.debug "UNHANDLED COMMAND $cmd" +} + +def on() { + log.info "on" + delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def off() { + delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value) { + def valueaux = value as Integer + def level = Math.min(valueaux, 99) + delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value, duration) { + def valueaux = value as Integer + def level = Math.min(valueaux, 99) + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format() +} + +def poll() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def indicatorWhenOn() { + sendEvent(name: "indicatorStatus", value: "when on", display: false) + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() +} + +def indicatorWhenOff() { + sendEvent(name: "indicatorStatus", value: "when off", display: false) + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() +} + +def indicatorNever() { + sendEvent(name: "indicatorStatus", value: "never", display: false) + zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() +} + +def invertSwitch(invert=true) { + if (invert) { + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 4, size: 1).format() + } + else { + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format() + } +} diff --git a/devicetypes/smartthings/door-shield.src/door-shield.groovy b/devicetypes/smartthings/door-shield.src/door-shield.groovy new file mode 100644 index 00000000000..91ab0586896 --- /dev/null +++ b/devicetypes/smartthings/door-shield.src/door-shield.groovy @@ -0,0 +1,50 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Door Shield", namespace: "smartthings", author: "SmartThings") { + + command "open" + } + + // simulator metadata + simulator { + // status messages + status "ping": "catchall: 0104 0000 01 01 0040 00 6A67 00 00 0000 0A 00 0A70696E67" + status "response": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A4F4D4E4F4D4E4F4D4E4F4D" + } + + // UI tile definitions + tiles { + standardTile("shield", "device.shield", width: 2, height: 2, canChangeBackground: true) { + state(name:"default", action:"open", icon:"st.shields.shields.door-shield", backgroundColor:"#ffffff") + } + + main "shield" + details "shield" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def value = zigbee.parse(description)?.text + def name = value && value != "ping" ? "response" : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +// Commands sent to the device +def open() { + zigbee.smartShield(text: "open sesame").format() +} diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy new file mode 100644 index 00000000000..39434c0ecad --- /dev/null +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -0,0 +1,675 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Ecobee Thermostat + * + * Author: SmartThings + * Date: 2013-06-13 + */ +metadata { + definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Thermostat" + capability "Polling" + capability "Sensor" + capability "Refresh" + + command "generateEvent" + command "raiseSetpoint" + command "lowerSetpoint" + command "resumeProgram" + command "switchMode" + + attribute "thermostatSetpoint","number" + attribute "thermostatStatus","string" + } + + simulator { } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "off", action:"switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off" + state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat" + state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool" + state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto" + state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat" + state "updating", label:"Working", icon: "st.secondary.secondary" + } + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on" + state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off" + state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate" + state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto" + } + standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { + state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up" + } + valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") { + state "thermostatSetpoint", label:'${currentValue}' + } + valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") { + state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff" + } + standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { + state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down" + } + controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue}° heat', unit:"F" + } + controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") { + state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon" + } + main "temperature" + details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) + } + +} + +/* + + preferences { + input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80 + input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70 + input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"] + } + +*/ + + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + // TODO: handle '' attribute + +} + +def refresh() +{ + log.debug "refresh called" + poll() + log.debug "refresh ended" +} + +def go() +{ + log.debug "before:go tile tapped" + poll() + log.debug "after" +} + +void poll() { + log.debug "Executing 'poll' using parent SmartApp" + + def results = parent.pollChild(this) + parseEventData(results) + generateStatusEvent() +} + +def parseEventData(Map results) +{ + log.debug "parsing data $results" + if(results) + { + results.each { name, value -> + + def linkText = getLinkText(device) + def isChange = false + def isDisplayed = true + + if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { + isChange = isTemperatureStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value, + unit: "F", + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + + } + else { + isChange = isStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value.toString(), + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + + } + } + generateSetpointEvent () + generateStatusEvent () + } +} + +void generateEvent(Map results) +{ + log.debug "parsing data $results" + if(results) + { + results.each { name, value -> + + def linkText = getLinkText(device) + def isChange = false + def isDisplayed = true + + if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { + isChange = isTemperatureStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value, + unit: "F", + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + } + else { + isChange = isStateChange(device, name, value.toString()) + isDisplayed = isChange + + sendEvent( + name: name, + value: value.toString(), + linkText: linkText, + descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name, + isStateChange: isChange, + displayed: isDisplayed) + + } + } + generateSetpointEvent () + generateStatusEvent() + } +} + +private getThermostatDescriptionText(name, value, linkText) +{ + if(name == "temperature") + { + return "$linkText was $value°F" + } + else if(name == "heatingSetpoint") + { + return "latest heating setpoint was $value°F" + } + else if(name == "coolingSetpoint") + { + return "latest cooling setpoint was $value°F" + } + else if (name == "thermostatMode") + { + return "thermostat mode is ${value}" + } + else + { + return "${name} = ${value}" + } +} + + +void setHeatingSetpoint(degreesF) { + setHeatingSetpoint(degreesF.toDouble()) +} + +void setHeatingSetpoint(Double degreesF) { + log.debug "setHeatingSetpoint({$degreesF})" + sendEvent("name":"heatingSetpoint", "value":degreesF) + Double coolingSetpoint = device.currentValue("coolingSetpoint") + log.debug "coolingSetpoint: $coolingSetpoint" + parent.setHold(this, degreesF, coolingSetpoint) +} + +void setCoolingSetpoint(degreesF) { + setCoolingSetpoint(degreesF.toDouble()) +} + +void setCoolingSetpoint(Double degreesF) { + log.debug "setCoolingSetpoint({$degreesF})" + sendEvent("name":"coolingSetpoint", "value":degreesF) + Double heatingSetpoint = device.currentValue("heatingSetpoint") + parent.setHold(this, heatingSetpoint, degreesF) +} + +def configure() { + +} + +def resumeProgram() { + parent.resumeProgram(this) +} + +def modes() { + if (state.modes) { + log.debug "Modes = ${state.modes}" + return state.modes + } + else { + state.modes = parent.availableModes(this) + log.debug "Modes = ${state.modes}" + return state.modes + } +} + +def fanModes() { + ["off", "on", "auto", "circulate"] +} + + +def switchMode() { + log.debug "in switchMode" + def currentMode = device.currentState("thermostatMode")?.value + def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" + def modeOrder = modes() + def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } + def nextMode = next(lastTriedMode) + switchToMode(nextMode) +} + +def switchToMode(nextMode) { + log.debug "In switchToMode = ${nextMode}" + if (nextMode in modes()) { + state.lastTriedMode = nextMode + "$nextMode"() + } else { + log.debug("no mode method '$nextMode'") + } +} + +def switchFanMode() { + def currentFanMode = device.currentState("thermostatFanMode")?.value + log.debug "switching fan from current mode: $currentFanMode" + def returnCommand + + switch (currentFanMode) { + case "fanAuto": + returnCommand = switchToFanMode("fanOn") + break + case "fanOn": + returnCommand = switchToFanMode("fanCirculate") + break + case "fanCirculate": + returnCommand = switchToFanMode("fanAuto") + break + } + if(!currentFanMode) { returnCommand = switchToFanMode("fanOn") } + returnCommand +} + +def switchToFanMode(nextMode) { + + log.debug "switching to fan mode: $nextMode" + def returnCommand + + if(nextMode == "fanAuto") { + if(!fanModes.contains("fanAuto")) { + returnCommand = fanAuto() + } else { + returnCommand = switchToFanMode("fanOn") + } + } else if(nextMode == "fanOn") { + if(!fanModes.contains("fanOn")) { + returnCommand = fanOn() + } else { + returnCommand = switchToFanMode("fanCirculate") + } + } else if(nextMode == "fanCirculate") { + if(!fanModes.contains("fanCirculate")) { + returnCommand = fanCirculate() + } else { + returnCommand = switchToFanMode("fanAuto") + } + } + returnCommand +} + +def getDataByName(String name) { + state[name] ?: device.getDataValue(name) +} + +def setThermostatMode(String value) { + log.debug "setThermostatMode({$value})" + +} + +def setThermostatFanMode(String value) { + + log.debug "setThermostatFanMode({$value})" + +} + +def generateModeEvent(mode) { + + sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true) + +} + +def generateFanModeEvent(fanMode) { + + sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true) + +} + +def generateOperatingStateEvent(operatingState) { + + sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true) + +} + +def off() { + log.debug "off" + generateModeEvent("off") + if (parent.setMode (this,"off")) + generateModeEvent("off") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() + +} + +def heat() { + log.debug "heat" + generateModeEvent("heat") + if (parent.setMode (this,"heat")) + generateModeEvent("heat") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() +} + +def auxHeatOnly() { + log.debug "auxHeatOnly" + generateModeEvent("auxHeatOnly") + if (parent.setMode (this,"auxHeatOnly")) + generateModeEvent("auxHeatOnly") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() +} + +def cool() { + log.debug "cool" + generateModeEvent("cool") + if (parent.setMode (this,"cool")) + generateModeEvent("cool") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() +} + +def auto() { + log.debug "auto" + generateModeEvent("auto") + if (parent.setMode (this,"auto")) + generateModeEvent("auto") + else { + log.debug "Error setting new mode." + def currentMode = device.currentState("thermostatMode")?.value + generateModeEvent(currentMode) // reset the tile back + } + generateSetpointEvent() + generateStatusEvent() +} + +def fanOn() { + log.debug "fanOn" + parent.setFanMode (this,"on") + +} + +def fanAuto() { + log.debug "fanAuto" + parent.setFanMode (this,"auto") + +} + +def fanCirculate() { + log.debug "fanCirculate" + parent.setFanMode (this,"circulate") + +} + +def fanOff() { + log.debug "fanOff" + parent.setFanMode (this,"off") + +} + +def generateSetpointEvent() { + + log.debug "Generate SetPoint Event" + + def mode = device.currentValue("thermostatMode") + log.debug "Current Mode = ${mode}" + + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + log.debug "Heating Setpoint = ${heatingSetpoint}" + + def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + log.debug "Cooling Setpoint = ${coolingSetpoint}" + + if (mode == "heat") { + + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") + + } + else if (mode == "cool") { + + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") + + } else if (mode == "auto") { + + sendEvent("name":"thermostatSetpoint", "value":"Auto") + + } else if (mode == "off") { + + sendEvent("name":"thermostatSetpoint", "value":"Off") + + } else if (mode == "emergencyHeat") { + + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") + + } + +} + +void raiseSetpoint() { + + log.debug "Raise SetPoint" + + def mode = device.currentValue("thermostatMode") + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + + log.debug "Current Mode = ${mode}" + + if (mode == "heat") { + + heatingSetpoint++ + + if (heatingSetpoint > 99) + heatingSetpoint = 99 + + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) + + parent.setHold (this, heatingSetpoint, coolingSetpoint) + + log.debug "New Heating Setpoint = ${heatingSetpoint}" + + } + else if (mode == "cool") { + + coolingSetpoint++ + + if (coolingSetpoint > 99) + coolingSetpoint = 99 + + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) + + parent.setHold (this, heatingSetpoint, coolingSetpoint) + + log.debug "New Cooling Setpoint = ${coolingSetpoint}" + + } + generateStatusEvent() + +} + + +void lowerSetpoint() { + log.debug "Lower SetPoint" + + def mode = device.currentValue("thermostatMode") + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + + log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}" + + if (mode == "heat" || mode == "emergencyHeat") { + + heatingSetpoint-- + + if (heatingSetpoint < 32) + heatingSetpoint = 32 + + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) + + parent.setHold (this, heatingSetpoint, coolingSetpoint) + + log.debug "New Heating Setpoint = ${heatingSetpoint}" + + } + else if (mode == "cool") { + + coolingSetpoint-- + + if (coolingSetpoint < 32) + coolingSetpoint = 32 + + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) + + parent.setHold (this, heatingSetpoint, coolingSetpoint) + + log.debug "New Cooling Setpoint = ${coolingSetpoint}" + + } + generateStatusEvent() +} + +def generateStatusEvent() { + + def mode = device.currentValue("thermostatMode") + def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def temperature = device.currentValue("temperature").toInteger() + + def statusText + + log.debug "Generate Status Event for Mode = ${mode}" + log.debug "Temperature = ${temperature}" + log.debug "Heating set point = ${heatingSetpoint}" + log.debug "Cooling set point = ${coolingSetpoint}" + log.debug "HVAC Mode = ${mode}" + + if (mode == "heat") { + + if (temperature >= heatingSetpoint) + statusText = "Right Now: Idle" + else + statusText = "Heating to ${heatingSetpoint}° F" + + } else if (mode == "cool") { + + if (temperature <= coolingSetpoint) + statusText = "Right Now: Idle" + else + statusText = "Cooling to ${coolingSetpoint}° F" + + } else if (mode == "auto") { + + statusText = "Right Now: Auto" + + } else if (mode == "off") { + + statusText = "Right Now: Off" + + } else if (mode == "emergencyHeat") { + + statusText = "Emergency Heat" + + } else { + + statusText = "?" + + } + log.debug "Generate Status Event = ${statusText}" + sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true) +} + diff --git a/devicetypes/smartthings/econet-vent.src/econet-vent.groovy b/devicetypes/smartthings/econet-vent.src/econet-vent.groovy new file mode 100644 index 00000000000..e2651b7372d --- /dev/null +++ b/devicetypes/smartthings/econet-vent.src/econet-vent.groovy @@ -0,0 +1,213 @@ +/** + * Econet EV100 Vent + * + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "EcoNet Vent", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Battery" + capability "Refresh" + capability "Sensor" + capability "Polling" + capability "Configuration" + + command "open" + command "close" + + fingerprint deviceId: "0x1100", inClusters: "0x26,0x72,0x86,0x77,0x80,0x20" + } + + simulator { + status "on": "command: 2603, payload: FF" + status "off": "command: 2603, payload: 00" + status "09%": "command: 2603, payload: 09" + status "10%": "command: 2603, payload: 0A" + status "33%": "command: 2603, payload: 21" + status "66%": "command: 2603, payload: 42" + status "99%": "command: 2603, payload: 63" + + // reply messages + reply "2001FF,delay 100,2602": "command: 2603, payload: FF" + reply "200100,delay 100,2602": "command: 2603, payload: 00" + reply "200119,delay 100,2602": "command: 2603, payload: 19" + reply "200132,delay 100,2602": "command: 2603, payload: 32" + reply "20014B,delay 100,2602": "command: 2603, payload: 4B" + reply "200163,delay 100,2602": "command: 2603, payload: 63" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0" + state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["switch","battery","refresh","levelSliderControl"]) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x32:3]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + result +} + +//send the command to stop polling +def updated() { + response("poll stop") +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) + [ response("poll stop") ] // we get a BasicReport when the hub starts polling +} + +//parse manufacture name and store it +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def dimmerEvents(physicalgraph.zwave.Command cmd) { + def text = "$device.displayName is ${cmd.value ? "open" : "closed"}" + def switchEvent = createEvent(name: "switch", value: (cmd.value ? "on" : "off"), descriptionText: text) + def levelEvent = createEvent(name:"level", value: cmd.value, unit:"%") + [switchEvent, levelEvent] +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ]) +} + +def open() { + on() +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ]) +} + +def close() { + off() +} + +def setLevel(value) { + delayBetween([ + zwave.basicV1.basicSet(value: value).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ]) +} + +def setLevel(value, duration) { + setLevel(value) +} + +def refresh() { + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + zwave.batteryV1.batteryGet().format() + ], 2200) +} + +//poll for battery once a day +def poll() { + + if (secondsPast(state.lastbatt, 36*60*60)) { + return zwave.batteryV1.batteryGet().format() + } else { + + return zwave.switchMultilevelV1.switchMultilevelGet().format() + } +} +def configure() { + [ + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ] + refresh() +} + +//check last message so battery poll doesn't happen all the time +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (new Date().time - timestamp) > (seconds * 1000) +} + +//Warn message for unkown events +def createEvents(physicalgraph.zwave.Command cmd) { + log.warn "UNEXPECTED COMMAND: $cmd" +} diff --git a/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy b/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy new file mode 100644 index 00000000000..68111fa4f07 --- /dev/null +++ b/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy @@ -0,0 +1,142 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Everspring Flood Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Water Sensor" + capability "Configuration" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: "0xA102", inClusters: "0x86,0x72,0x85,0x84,0x80,0x70,0x9C,0x20,0x71" + } + + simulator { + status "dry": "command: 9C02, payload: 00 05 00 00 00" + status "wet": "command: 9C02, payload: 00 05 FF 00 00" + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage() + } + } + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + main "water" + details(["water", "battery", "configure"]) + } +} + +def parse(String description) { + def result = null + def parsedZwEvent = zwave.parse(description, [0x9C: 1, 0x71: 1, 0x84: 2, 0x30: 1]) + if (parsedZwEvent) { + result = zwaveEvent(parsedZwEvent) + } + log.debug "Parse '${description}' returned ${result}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) +{ + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + def now = new Date().time + if (!state.battreq || now - state.battreq > 53*60*60*1000) { + state.battreq = now + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + def map = [:] + if (cmd.sensorType == 0x05) { + map.name = "water" + map.value = cmd.sensorState ? "wet" : "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + } else { + map.descriptionText = "${device.displayName}: ${cmd}" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + def map = [:] + map.name = "water" + map.value = cmd.sensorValue ? "wet" : "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) +{ + def map = [:] + if (cmd.alarmType == 1 && cmd.alarmLevel == 0xFF) { + map.name = "battery" + map.value = 1 + map.unit = "%" + map.descriptionText = "${device.displayName} has a low battery" + map.displayed = true + map + } else if (cmd.alarmType == 2 && cmd.alarmLevel == 1) { + map.descriptionText = "${device.displayName} powered up" + map.displayed = false + map + } else { + map.descriptionText = "${device.displayName}: ${cmd}" + map.displayed = false + } + createEvent(map) +} + + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + if (cmd.batteryLevel == 0xFF) { + map.name = "battery" + map.value = 1 + map.unit = "%" + map.descriptionText = "${device.displayName} has a low battery" + map.displayed = true + } else { + map.name = "battery" + map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 + map.unit = "%" + map.displayed = false + } + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) +{ + createEvent(descriptionText: "${device.displayName}: ${cmd}", displayed: false) +} + +def configure() +{ + if (!device.currentState("battery")) { + sendEvent(name: "battery", value:100, unit:"%", descriptionText:"(Default battery event)", displayed:false) + } + zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]).format() +} diff --git a/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy b/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy new file mode 100644 index 00000000000..1c9081a3c14 --- /dev/null +++ b/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy @@ -0,0 +1,334 @@ +/** + * Device Type Definition File + * + * Device Type: Fibaro Dimmer + * File Name: fibaro-dimmer.groovy + * Initial Release: 2015-06-00 + * Author: SmartThings + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +metadata { + // Automatically generated. Make future change here. + definition (name: "Fibaro Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "resetParams2StDefaults" + command "listCurrentParams" + command "updateZwaveParam" + + fingerprint deviceId: "0x1101", inClusters: "0x72,0x86,0x70,0x85,0x8E,0x26,0x7A,0x27,0x73,0xEF,0x26,0x2B" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main(["switch"]) + details(["switch", "refresh", "levelSliderControl"]) + } +} + +def parse(String description) { + def item1 = [ + canBeCurrentState: false, + linkText: getLinkText(device), + isStateChange: false, + displayed: false, + descriptionText: description, + value: description + ] + def result + def cmd = zwave.parse(description, [0x26: 1, 0x70: 2, 072: 2]) + //log.debug "cmd: ${cmd}" + + if (cmd) { + result = createEvent(cmd, item1) + } + else { + item1.displayed = displayed(description, item1.isStateChange) + result = [item1] + } + + if(result?.descriptionText) + log.debug "Parse returned ${result?.descriptionText}" + + result + +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) { + [] +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) { + [response(zwave.basicV1.basicGet())] +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + result[0].descriptionText = "${item1.linkText} is ${item1.value}" + result[0].handlerName = cmd.value ? "statusOn" : "statusOff" + for (int i = 0; i < result.size(); i++) { + result[i].type = "digital" + } + result +} + +def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { + def result = [item1] + + item1.name = "switch" + item1.value = cmd.value ? "on" : "off" + item1.handlerName = item1.value + item1.descriptionText = "${item1.linkText} was turned ${item1.value}" + item1.canBeCurrentState = true + item1.isStateChange = isStateChange(device, item1.name, item1.value) + item1.displayed = item1.isStateChange + + if (cmd.value >= 5) { + def item2 = new LinkedHashMap(item1) + item2.name = "level" + item2.value = cmd.value as String + item2.unit = "%" + item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" + item2.canBeCurrentState = true + item2.isStateChange = isStateChange(device, item2.name, item2.value) + item2.displayed = false + result << item2 + } + result +} + +def createEvent(physicalgraph.zwave.Command cmd, Map map) { + // Handles any Z-Wave commands we aren't interested in + log.debug "UNHANDLED COMMAND $cmd" +} + +def on() { + log.info "on" + delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def off() { + delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value) { + def level = Math.min(value as Integer, 99) + delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value, duration) { + def level = Math.min(value as Integer, 99) + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format() +} + +def poll() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + + /** + * Configures the device to settings needed by SmarthThings at device discovery time. Assumes + * device is already at default parameter settings. + * + * @param none + * + * @return none + */ +def configure() { + log.debug "Configuring Device..." + def cmds = [] + + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() + + + delayBetween(cmds, 500) +} + + +def createEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd, Map item1) { + + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + +} + + /** + * This method will allow the user to update device parameters (behavior) from an app. + * the user can write his/her own app to envoke this method. + * No type or value checking is done to compare to what device capability or reaction. + * It is up to user to read OEM documentation prio to envoking this method. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param List[paramNumber:80,value:10,size:1] + * + * + * @return none + */ +def updateZwaveParam(params) { + if ( params ) { + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Make sure device is awake and in recieve mode" + log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1000) + } +} + + /** + * Sets all of available Fibaro parameters back to the device defaults except for what + * SmartThings needs to support the stock functionality as released. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def resetParams2StDefaults() { + log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults" + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 1, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 6, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 7, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 8, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 9, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 10, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 11, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [99], parameterNumber: 12, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 13, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 14, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 15, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 16, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 17, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 18, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 19, size: 1).format() + //Param 20 is different for 50Hz or 60 Hz uncomment the line that will reflect the power frequency used + //cmds << zwave.configurationV1.configurationSet(configurationValue: [101], parameterNumber: 20, size: 1).format() //60 Hz (US) + //cmds << zwave.configurationV1.configurationSet(configurationValue: [110], parameterNumber: 20, size: 1).format() //50 Hz (UK) + cmds << zwave.configurationV1.configurationSet(configurationValue: [3], parameterNumber: 30, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [600], parameterNumber: 39, size: 1).format() + //Param 40 not needed by SmartThings + //cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 40, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 41, size: 1).format() + + delayBetween(cmds, 500) +} + + /** + * Lists all of available Fibaro parameters and thier current settings out to the + * logging window in the IDE. This will be called from the "Fibaro Tweaker" or + * user's own app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def listCurrentParams() { + log.debug "Listing of current parameter settings of ${device.displayName}" + def cmds = [] + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 11).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 15).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 16).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 17).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 18).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 19).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 20).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 30).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 39).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 40).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 41).format() + + delayBetween(cmds, 500) +} diff --git a/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy b/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy new file mode 100644 index 00000000000..8970554622d --- /dev/null +++ b/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy @@ -0,0 +1,385 @@ +/** + * Device Type Definition File + * + * Device Type: Fibaro Door/Window Sensor + * File Name: fibaro-door-window-sensor.groovy + * Initial Release: 2014-12-10 + * @author: Todd Wackford + * Email: todd@wackford.net + * @version: 1.0 + * + * Copyright 2014 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. + * + */ + + /** + * Sets up metadata, simulator info and tile definition. The tamper tile is setup, but + * not displayed to the user. We do this so we can receive events and display on device + * activity. If the user wants to display the tamper tile, adjust the tile display lines + * with the following: + * main(["contact", "temperature", "tamper"]) + * details(["contact", "temperature", "battery", "tamper"]) + * + * @param none + * + * @return none + */ + metadata { + definition (name: "Fibaro Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { + //capability "Temperature Measurement" //UNCOMMENT ME IF TEMP INSTALLED + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + + command "resetParams2StDefaults" + command "listCurrentParams" + command "updateZwaveParam" + command "test" + + fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x85,0x72,0x70,0x86,0x80,0x56,0x84,0x7A,0xEF,0x2B" + } + + simulator { + // messages the device returns in response to commands it receives + status "open" : "command: 2001, payload: FF" + status "closed": "command: 2001, payload: 00" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + } + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: "", color: "#ffffff"], + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + standardTile("tamper", "device.alarm") { + state("secure", label:'secure', icon:"st.locks.lock.locked", backgroundColor:"#ffffff") + state("tampered", label:'tampered', icon:"st.locks.lock.unlocked", backgroundColor:"#53a7c0") + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + //this will display a temperature tile for the DS18B20 sensor + //main(["contact", "temperature"]) //COMMENT ME OUT IF NO TEMP INSTALLED + //details(["contact", "temperature", "battery"]) //COMMENT ME OUT IF NO TEMP INSTALLED + + //this will hide the temperature tile if the DS18B20 sensor is not installed + main(["contact"]) //UNCOMMENT ME IF NO TEMP INSTALLED + details(["contact", "battery"]) //UNCOMMENT ME IF NO TEMP INSTALLED + } +} + +// Parse incoming device messages to generate events +def parse(String description) +{ + def result = [] + def cmd = zwave.parse(description, [0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x72: 2, 0x56: 1, 0x60: 3]) + if (cmd) { + result += zwaveEvent(cmd) + } + log.debug "parsed '$description' to ${result.inspect()}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x72: 2, 0x60: 3] + // def encapsulatedCommand = cmd.encapsulatedCommand(versions) + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + log.debug "Could not extract command from $cmd" + } else { + return zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 2, 0x31: 2]) // can specify command class versions here like in zwave.parse + log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + if (!state.lastbat || now() - state.lastbat > 24*60*60*1000) { + cmds << zwave.batteryV1.batteryGet().format() + } else { + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + [event, response(cmds)] +} + + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + break; + } + createEvent(map) +} + + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = now() + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + def map = [:] + map.value = cmd.sensorValue ? "open" : "closed" + map.name = "contact" + if (map.value == "closed") { + map.descriptionText = "$device.displayName is closed" + } + else { + map.descriptionText = "$device.displayName is open" + } + createEvent(map) +} + +// added so UK (non-multichannel) and US device supported by same device file. +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + def map = [:] + map.value = cmd.sensorState ? "tampered" : "secure" + map.name = "tamper" + if (map.value == "tampered") { + map.descriptionText = "$device.displayName has been tampered with" + } + else { + map.descriptionText = "$device.displayName is secure" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Catchall reached for cmd: ${cmd.toString()}}" + [] +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def result = [] + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + + if (cmd.parameterNumber == 15) { + if (cmd.configurationValue[0] == 1) { //error in temp probe + result << createEvent(name:"temperature", value:"-99") + } else if (cmd.configurationValue[0] == 255) { //no temp probe + result << createEvent(name:"temperature", value:"") + } + result += response(zwave.batteryV1.batteryGet().format()) // send this after configure() runs + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + device.updateDataValue(["MSR", msr]) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + + /** + * Configures the device to settings needed by SmarthThings at device discovery time. + * + * @param none + * + * @return none + */ +def configure() { + log.debug "Configuring Device..." + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format() + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() + + // send associate to group 2 to get tamper alarm data reported + cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format() + + // turn on the tamper alarm + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 10, size: 1).format() + //cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + + // temperature change sensitivity + cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 12, size: 1).format() + //cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + + // remove group 1 association to stop redundant BasicSet + cmds << zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format() + + // see if there is a temp probe on board and is it working + cmds << zwave.configurationV1.configurationGet(parameterNumber: 15).format() + + delayBetween(cmds, 500) +} + +//used to add "test" button for simulation of user changes to parameters +def test() { + def params = [paramNumber:10,value:1,size:1] + updateZwaveParam(params) + //zwave.wakeUpV1.wakeUpIntervalSet(seconds: 30, nodeid:zwaveHubNodeId).format() +} + + /** + * This method will allow the user to update device parameters (behavior) from an app. + * A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can + * write his/her own app to envoke this method. No type or value checking is done to + * compare to what device capability or reaction. It is up to user to read OEM + * documentation prio to envoking this method. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param List[paramNumber:80,value:10,size:1] + * + * + * @return none + */ +def updateZwaveParam(params) { + if ( params ) { + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Make sure device is awake and in recieve mode" + log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1000) + } +} + + /** + * Sets all of available Fibaro parameters back to the device defaults except for what + * SmartThings needs to support the stock functionality as released. This will be + * called from the "Fibaro Tweaker" or user's app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def resetParams2StDefaults() { + log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults" + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 2, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 7, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 9, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 10, size: 1).format() //ST Custom + cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 12, size: 1).format() //St Custom + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 13, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 14, size: 1).format() + + delayBetween(cmds, 500) +} + + /** + * Lists all of available Fibaro parameters and thier current settings out to the + * logging window in the IDE. This will be called from the "Fibaro Tweaker" or + * user's own app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def listCurrentParams() { + log.debug "Listing of current parameter settings of ${device.displayName}" + def cmds = [] + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 15).format() + + delayBetween(cmds, 500) +} diff --git a/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy b/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy new file mode 100644 index 00000000000..7759d853b07 --- /dev/null +++ b/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy @@ -0,0 +1,437 @@ +/** + * Device Type Definition File + * + * Device Type: Fibaro Flood Sensor + * File Name: fibaro-flood-sensor.groovy + * Initial Release: 2014-12-10 + * @author: Todd Wackford + * Email: todd@wackford.net + * @version: 1.0 + * + * Copyright 2014 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. + * + */ + + /** + * Sets up metadata, simulator info and tile definition. The tamper tile is setup, but + * not displayed to the user. We do this so we can receive events and display on device + * activity. If the user wants to display the tamper tile, adjust the tile display lines + * with the following: + * main(["water", "temperature", "tamper"]) + * details(["water", "temperature", "battery", "tamper"]) + * + * @param none + * + * @return none + */ +metadata { + definition (name: "Fibaro Flood Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Water Sensor" + capability "Temperature Measurement" + capability "Configuration" + capability "Battery" + + command "resetParams2StDefaults" + command "listCurrentParams" + command "updateZwaveParam" + command "test" + + fingerprint deviceId: "0xA102", inClusters: "0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84" + } + + simulator { + // messages the device returns in response to commands it receives + status "motion (basic)" : "command: 2001, payload: FF" + status "no motion (basic)" : "command: 2001, payload: 00" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + + for (int i = 200; i <= 1000; i += 200) { + status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + } + + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + standardTile("tamper", "device.tamper") { + state("secure", label:"secure", icon:"st.locks.lock.locked", backgroundColor:"#ffffff") + state("tampered", label:"tampered", icon:"st.locks.lock.unlocked", backgroundColor:"#53a7c0") + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main(["water", "temperature"]) + details(["water", "temperature", "battery", "configure"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) +{ + def result = [] + + if (description == "updated") { + if (!state.MSR) { + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 60*60, nodeid:zwaveHubNodeId)) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + } + } else { + def cmd = zwave.parse(description, [0x31: 2, 0x30: 1, 0x70: 2, 0x71: 1, 0x84: 1, 0x80: 1, 0x9C: 1, 0x72: 2, 0x56: 2, 0x60: 3]) + + if (cmd) { + result += zwaveEvent(cmd) //createEvent(zwaveEvent(cmd)) + } + } + + result << response(zwave.batteryV1.batteryGet().format()) + + if ( result[0] != null ) { + log.debug "Parse returned ${result}" + result + } +} + + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x30: 2, 0x31: 2]) // can specify command class versions here like in zwave.parse + log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } +} + +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + if (!isConfigured()) { + // we're still in the process of configuring a newly joined device + result += lateConfigure(true) + } else { + result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + log.debug "We're done with WakeUp!" + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) +{ + def map = [:] + + switch (cmd.sensorType) { + case 1: + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + break; + case 0: + // here's our tamper alarm = acceleration + map.value = cmd.sensorState == 255 ? "active" : "inactive" + map.name = "acceleration" + break; + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 + map.unit = "%" + map.displayed = false + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + def map = [:] + map.value = cmd.sensorValue ? "active" : "inactive" + map.name = "acceleration" + + if (map.value == "active") { + map.descriptionText = "$device.displayName detected vibration" + } + else { + map.descriptionText = "$device.displayName vibration has stopped" + } + createEvent(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.basicv1.BasicSet cmd) { + log.debug "BasicSet with CMD = ${cmd}" + + if (!isConfigured()) { + def result = [] + def map = [:] + + map.name = "water" + map.value = cmd.value ? "wet" : "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + + // If we are getting a BasicSet, and isConfigured == false, then we are likely NOT properly configured. + result += lateConfigure(true) + + result << createEvent(map) + + result + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + def map = [:] + + if (cmd.sensorType == 0x05) { + map.name = "water" + map.value = cmd.sensorState ? "wet" : "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + + log.debug "CMD = SensorAlarmReport: ${cmd}" + setConfigured() + } else if ( cmd.sensorType == 0) { + map.name = "tamper" + map.isStateChange = true + map.value = cmd.sensorState ? "tampered" : "secure" + map.descriptionText = "${device.displayName} has been tampered with" + runIn(30, "resetTamper") //device does not send alarm cancelation + + } else if ( cmd.sensorType == 1) { + map.name = "tamper" + map.value = cmd.sensorState ? "tampered" : "secure" + map.descriptionText = "${device.displayName} has been tampered with" + runIn(30, "resetTamper") //device does not send alarm cancelation + + } else { + map.descriptionText = "${device.displayName}: ${cmd}" + } + createEvent(map) +} + +def resetTamper() { + def map = [:] + map.name = "tamper" + map.value = "secure" + map.descriptionText = "$device.displayName is secure" + sendEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Catchall reached for cmd: ${cmd.toString()}}" + [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + device.updateDataValue(["MSR", msr]) + + if ( msr == "010F-0B00-2001" ) { //this is the msr and device type for the fibaro flood sensor + result += lateConfigure(true) + } + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def setConfigured() { + device.updateDataValue("configured", "true") +} + +def isConfigured() { + Boolean configured = device.getDataValue(["configured"]) as Boolean + + return configured +} + +def lateConfigure(setConf = False) { + def res = response(configure()) + + if (setConf) + setConfigured() + + return res +} + + /** + * Configures the device to settings needed by SmarthThings at device discovery time. + * + * @param none + * + * @return none + */ +def configure() { + log.debug "Configuring Device..." + def cmds = [] + + // send associate to group 2 to get alarm data + cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format() + + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() + + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() + + // temp hysteresis set to .5 degrees celcius + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + + // reporting frequency of temps and battery set to one hour + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + + delayBetween(cmds, 100) +} + + +//used to add "test" button for simulation of user changes to parameters +def test() { + def params = [paramNumber:12,value:4,size:1] + updateZwaveParam(params) +} + + /** + * This method will allow the user to update device parameters (behavior) from an app. + * A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can + * write his/her own app to envoke this method. No type or value checking is done to + * compare to what device capability or reaction. It is up to user to read OEM + * documentation prio to envoking this method. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param List[paramNumber:80,value:10,size:1] + * + * + * @return none + */ +def updateZwaveParam(params) { + if ( params ) { + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Make sure device is awake and in recieve mode (triple-click?)" + log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1000) + } +} + + /** + * Sets all of available Fibaro parameters back to the device defaults except for what + * SmartThings needs to support the stock functionality as released. This will be + * called from the "Fibaro Tweaker" or user's app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def resetParams2StDefaults() { + log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults" + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [3], parameterNumber: 2, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 7, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 9, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 13, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [5,220], parameterNumber: 50, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [13,172], parameterNumber: 51, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0,0,225], parameterNumber: 61, size: 4).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,255,0,0], parameterNumber: 62, size: 4).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 63, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 73, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 74, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 75, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 76, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 77, size: 1).format() + + delayBetween(cmds, 1200) +} + + /** + * Lists all of available Fibaro parameters and thier current settings out to the + * logging window in the IDE This will be called from the "Fibaro Tweaker" or + * user's own app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def listCurrentParams() { + log.debug "Listing of current parameter settings of ${device.displayName}" + def cmds = [] + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 50).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 51).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 61).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 63).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 73).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 74).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 75).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 76).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 77).format() + + delayBetween(cmds, 1200) +} + diff --git a/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy b/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy new file mode 100644 index 00000000000..96258c3a200 --- /dev/null +++ b/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy @@ -0,0 +1,435 @@ +/** + * Device Type Definition File + * + * Device Type: Fibaro Motion Sensor + * File Name: fibaro-motion-sensor.groovy + * Initial Release: 2014-12-10 + * Author: Todd Wackford + * Email: todd@wackford.net + * + * Copyright 2014 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. + * + *************************************************************************************** + * + * Change Log: + * + * 1. 20150125 Todd Wackford + * Incorporated Crc16Encap function to support core code changes. Duncan figured it + * out as usual. + * + * 2. 20150125 Todd Wackford + * Leaned out parse and moved most device info getting into configuration method. + */ + + /** + * Sets up metadata, simulator info and tile definition. + * + * @param none + * + * @return none + */ + metadata { + definition (name: "Fibaro Motion Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Acceleration Sensor" + capability "Configuration" + capability "Illuminance Measurement" + capability "Sensor" + capability "Battery" + + command "resetParams2StDefaults" + command "listCurrentParams" + command "updateZwaveParam" + command "test" + command "configure" + + fingerprint deviceId: "0x2001", inClusters: "0x30,0x84,0x85,0x80,0x8F,0x56,0x72,0x86,0x70,0x8E,0x31,0x9C,0xEF,0x30,0x31,0x9C" + } + + simulator { + // messages the device returns in response to commands it receives + status "motion (basic)" : "command: 2001, payload: FF" + status "no motion (basic)" : "command: 2001, payload: 00" + status "motion (binary)" : "command: 3003, payload: FF" + status "no motion (binary)" : "command: 3003, payload: 00" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + + for (int i = 200; i <= 1000; i += 200) { + status "luminance ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + standardTile("acceleration", "device.acceleration") { + state("active", label:'vibration', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'still', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + + + main(["motion", "temperature", "acceleration", "illuminance"]) + details(["motion", "temperature", "acceleration", "battery", "illuminance", "configure"]) + } +} + + /** + * Configures the device to settings needed by SmarthThings at device discovery time. + * + * @param none + * + * @return none + */ +def configure() { + log.debug "Configuring Device For SmartThings Use" + def cmds = [] + + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() + + // turn on tamper sensor with active/inactive reports (use it as an acceleration sensor) default is 0, or off + cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format() + + // temperature change report threshold (0-255 = 0.1 to 25.5C) default is 1.0 Celcius, setting to .5 Celcius + cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format() + + cmds << response(zwave.batteryV1.batteryGet()) + cmds << response(zwave.versionV1.versionGet().format()) + cmds << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + cmds << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) + + delayBetween(cmds, 500) +} + +// Parse incoming device messages to generate events +def parse(String description) +{ + def result = [] + def cmd = zwave.parse(description, [0x72: 2, 0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x86: 1, 0x7A: 1, 0x56: 1]) + + if (description == "updated") { + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 7200, nodeid:zwaveHubNodeId)) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + } + + if (cmd) { + if( cmd.CMD == "8407" ) { + result << response(zwave.batteryV1.batteryGet().format()) + result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + } + result << createEvent(zwaveEvent(cmd)) + } + + if ( result[0] != null ) { + log.debug "Parse returned ${result}" + result + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + // def encapsulatedCommand = cmd.encapsulatedCommand(versions) + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (!encapsulatedCommand) { + log.debug "Could not extract command from $cmd" + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) { + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) { + updateDataValue("applicationVersion", "${cmd.applicationVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def createEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd, Map item1) { + log.debug "checksum: ${cmd.checksum}" + log.debug "firmwareId: ${cmd.firmwareId}" + log.debug "manufacturerId: ${cmd.manufacturerId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + def map = [:] + map.name = "acceleration" + + map.value = cmd.sensorState ? "active" : "inactive" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected vibration" + } + else { + map.descriptionText = "$device.displayName vibration has stopped" + } + map +} + +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + [descriptionText: "${device.displayName} woke up", isStateChange: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + break; + case 3: + // luminance + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + map.name = "illuminance" + break; + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { +log.debug cmd + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 + map.unit = "%" + map.displayed = false + map +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + def map = [:] + map.value = cmd.sensorValue ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected motion" + } + else { + map.descriptionText = "$device.displayName motion has stopped" + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + def map = [:] + map.value = cmd.value ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected motion" + } + else { + map.descriptionText = "$device.displayName motion has stopped" + } + map +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Catchall reached for cmd: ${cmd.toString()}}" + [:] +} + +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.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + if ( msr == "010F-0800-2001" ) { //this is the msr and device type for the fibaro motion sensor + configure() + } + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +//used to add "test" button for simulation of user changes to parameters +def test() { + def params = [paramNumber:80,value:10,size:1] + updateZwaveParam(params) +} + + /** + * This method will allow the user to update device parameters (behavior) from an app. + * A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can + * write his/her own app to envoke this method. No type or value checking is done to + * compare to what device capability or reaction. It is up to user to read OEM + * documentation prio to envoking this method. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param List[paramNumber:80,value:10,size:1] + * + * + * @return none + */ +def updateZwaveParam(params) { + if ( params ) { + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Make sure device is awake and in recieve mode" + log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1000) + } +} + + /** + * Sets all of available Fibaro parameters back to the device defaults except for what + * SmartThings needs to support the stock functionality as released. This will be + * called from the "Fibaro Tweaker" or user's app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def resetParams2StDefaults() { + log.debug "Resetting Sensor Parameters to SmartThings Compatible Defaults" + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: [10], parameterNumber: 1, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [15], parameterNumber: 2, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 4, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,30], parameterNumber: 6, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 8, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,200], parameterNumber: 9, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 12, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 16, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [15], parameterNumber: 20, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,30], parameterNumber: 22, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 26, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,200], parameterNumber: 40, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 42, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [3,132], parameterNumber: 62, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 64, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 66, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [10], parameterNumber: 80, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [50], parameterNumber: 81, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,100], parameterNumber: 82, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [3,232], parameterNumber: 83, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [18], parameterNumber: 86, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [28], parameterNumber: 87, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 89, size: 1).format() + + delayBetween(cmds, 500) +} + + /** + * Lists all of available Fibaro parameters and thier current settings out to the + * logging window in the IDE This will be called from the "Fibaro Tweaker" or + * user's own app. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param none + * + * @return none + */ +def listCurrentParams() { + log.debug "Listing of current parameter settings of ${device.displayName}" + def cmds = [] + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 14).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 16).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 20).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 22).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 26).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 40).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 42).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 64).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 66).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 80).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 81).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 82).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 83).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 86).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 87).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 89).format() + + delayBetween(cmds, 500) +} + diff --git a/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy b/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy new file mode 100644 index 00000000000..89e842bf4e9 --- /dev/null +++ b/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy @@ -0,0 +1,864 @@ +/** + * Device Type Definition File + * + * Device Type: Fibaro RGBW Controller + * File Name: fibaro-rgbw-controller.groovy + * Initial Release: 2015-01-04 + * Author: Todd Wackford + * Email: todd@wackford.net + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + + metadata { + + definition (name: "Fibaro RGBW Controller", namespace: "smartthings", author: "Todd Wackford") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Configuration" + capability "Color Control" + capability "Power Meter" + + command "getDeviceData" + command "softwhite" + command "daylight" + command "warmwhite" + command "red" + command "green" + command "blue" + command "cyan" + command "magenta" + command "orange" + command "purple" + command "yellow" + command "white" + command "fireplace" + command "storm" + command "deepfade" + command "litefade" + command "police" + command "setAdjustedColor" + command "setWhiteLevel" + command "test" + + attribute "whiteLevel", "string" + + fingerprint deviceId: "0x1101", inClusters: "0x27,0x72,0x86,0x26,0x60,0x70,0x32,0x31,0x85,0x33" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" + } + + tiles { + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setAdjustedColor" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + controlTile("whiteSliderControl", "device.whiteLevel", "slider", height: 1, width: 3, inactiveLabel: false) { + state "whiteLevel", action:"setWhiteLevel", label:'White Level' + } + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', icon:"st.illuminance.illuminance.bright", backgroundColor:"#79b821" + state "turningOff", label:'${name}', icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff" + } + valueTile("power", "device.power", decoration: "flat") { + state "power", label:'${currentValue} W' + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + standardTile("refresh", "device.switch", height: 1, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("softwhite", "device.softwhite", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF1E0" + } + standardTile("daylight", "device.daylight", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offdaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "ondaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFB" + } + standardTile("warmwhite", "device.warmwhite", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF4E5" + } + standardTile("red", "device.red", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offred", label:"red", action:"red", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onred", label:"red", action:"red", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF0000" + } + standardTile("green", "device.green", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offgreen", label:"green", action:"green", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "ongreen", label:"green", action:"green", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FF00" + } + standardTile("blue", "device.blue", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.bright", backgroundColor:"#0000FF" + } + standardTile("cyan", "device.cyan", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offcyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "oncyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FFFF" + } + standardTile("magenta", "device.magenta", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF00FF" + } + standardTile("orange", "device.orange", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF6600" + } + standardTile("purple", "device.purple", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.bright", backgroundColor:"#BF00FF" + } + standardTile("yellow", "device.yellow", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFF00" + } + standardTile("white", "device.white", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("fireplace", "device.fireplace", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("storm", "device.storm", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("deepfade", "device.deepfade", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offdeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "ondeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("litefade", "device.litefade", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("police", "device.police", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { + state "saturation", action:"color control.setSaturation" + } + valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { + state "saturation", label: 'Sat ${currentValue} ' + } + controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { + state "hue", action:"color control.setHue" + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } + + main(["switch"]) + details(["switch", + "levelSliderControl", + "rgbSelector", + "whiteSliderControl", + /*"softwhite", + "daylight", + "warmwhite", + "red", + "green", + "blue", + "white", + "cyan", + "magenta", + "orange", + "purple", + "yellow", + "fireplace", + "storm", + "deepfade", + "litefade", + "police", + "power", + "configure",*/ + "refresh"]) + } +} + +def setAdjustedColor(value) { + log.debug "setAdjustedColor: ${value}" + + toggleTiles("off") //turn off the hard color tiles + + def level = device.latestValue("level") + if(level == null) + level = 50 + log.debug "level is: ${level}" + value.level = level + + def c = hexToRgb(value.hex) + value.rh = hex(c.r * (level/100)) + value.gh = hex(c.g * (level/100)) + value.bh = hex(c.b * (level/100)) + + setColor(value) +} + +def setColor(value) { + log.debug "setColor: ${value}" + log.debug "hue is: ${value.hue}" + log.debug "saturation is: ${value.saturation}" + + if (value.size() < 8) + toggleTiles("off") + + if (( value.size() == 2) && (value.hue != null) && (value.saturation != null)) { //assuming we're being called from outside of device (App) + def rgb = hslToRGB(value.hue, value.saturation, 0.5) + value.hex = rgbToHex(rgb) + value.rh = hex(rgb.r) + value.gh = hex(rgb.g) + value.bh = hex(rgb.b) + } + + if ((value.size() == 3) && (value.hue != null) && (value.saturation != null) && (value.level)) { //user passed in a level value too from outside (App) + def rgb = hslToRGB(value.hue, value.saturation, 0.5) + value.hex = rgbToHex(rgb) + value.rh = hex(rgb.r * value.level/100) + value.gh = hex(rgb.g * value.level/100) + value.bh = hex(rgb.b * value.level/100) + } + + if (( value.size() == 1) && (value.hex)) { //being called from outside of device (App) with only hex + def rgbInt = hexToRgb(value.hex) + value.rh = hex(rgbInt.r) + value.gh = hex(rgbInt.g) + value.bh = hex(rgbInt.b) + } + + if (( value.size() == 2) && (value.hex) && (value.level)) { //being called from outside of device (App) with only hex and level + + def rgbInt = hexToRgb(value.hex) + value.rh = hex(rgbInt.r * value.level/100) + value.gh = hex(rgbInt.g * value.level/100) + value.bh = hex(rgbInt.b * value.level/100) + } + + if (( value.size() == 1) && (value.colorName)) { //being called from outside of device (App) with only color name + def colorData = getColorData(value.colorName) + value.rh = colorData.rh + value.gh = colorData.gh + value.bh = colorData.bh + value.hex = "#${value.rh}${value.gh}${value.bh}" + } + + if (( value.size() == 2) && (value.colorName) && (value.level)) { //being called from outside of device (App) with only color name and level + def colorData = getColorData(value.colorName) + value.rh = hex(colorData.r * value.level/100) + value.gh = hex(colorData.g * value.level/100) + value.bh = hex(colorData.b * value.level/100) + value.hex = "#${hex(colorData.r)}${hex(colorData.g)}${hex(colorData.b)}" + } + + if (( value.size() == 3) && (value.red != null) && (value.green != null) && (value.blue != null)) { //being called from outside of device (App) with only color values (0-255) + value.rh = hex(value.red) + value.gh = hex(value.green) + value.bh = hex(value.blue) + value.hex = "#${value.rh}${value.gh}${value.bh}" + } + + if (( value.size() == 4) && (value.red != null) && (value.green != null) && (value.blue != null) && (value.level)) { //being called from outside of device (App) with only color values (0-255) and level + value.rh = hex(value.red * value.level/100) + value.gh = hex(value.green * value.level/100) + value.bh = hex(value.blue * value.level/100) + value.hex = "#${hex(value.red)}${hex(value.green)}${hex(value.blue)}" + } + + sendEvent(name: "hue", value: value.hue, displayed: false) + sendEvent(name: "saturation", value: value.saturation, displayed: false) + sendEvent(name: "color", value: value.hex, displayed: false) + if (value.level) { + sendEvent(name: "level", value: value.level) + } + if (value.switch) { + sendEvent(name: "switch", value: value.switch) + } + + sendRGB(value.rh, value.gh, value.bh) +} + +def setLevel(level) { + log.debug "setLevel($level)" + + if (level == 0) { off() } + else if (device.latestValue("switch") == "off") { on() } + + def colorHex = device.latestValue("color") + if (colorHex == null) + colorHex = "#FFFFFF" + + def c = hexToRgb(colorHex) + + def r = hex(c.r * (level/100)) + def g = hex(c.g * (level/100)) + def b = hex(c.b * (level/100)) + + sendEvent(name: "level", value: level) + sendEvent(name: "setLevel", value: level, displayed: false) + sendRGB(r, g, b) +} + + +def setWhiteLevel(value) { + log.debug "setWhiteLevel: ${value}" + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + def channel = 0 + + if (device.latestValue("switch") == "off") { on() } + + sendEvent(name: "whiteLevel", value: value) + sendWhite(channel, value) +} + +def sendWhite(channel, value) { + def whiteLevel = hex(value) + def cmd = [String.format("3305010${channel}${whiteLevel}%02X", 50)] + cmd +} + +def sendRGB(redHex, greenHex, blueHex) { + def cmd = [String.format("33050302${redHex}03${greenHex}04${blueHex}%02X", 100),] + cmd +} + + +def sendRGBW(redHex, greenHex, blueHex, whiteHex) { + def cmd = [String.format("33050400${whiteHex}02${redHex}03${greenHex}04${blueHex}%02X", 100),] + cmd +} + + +def configure() { + log.debug "Configuring Device For SmartThings Use" + + + + def cmds = [] + + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format() + + + //cmds << sendEvent(name: "level", value: 50) + //cmds << on() + //cmds << doColorButton("Green") + delayBetween(cmds, 500) + +} + +def parse(String description) { + //log.debug "description: ${description}" + def item1 = [ + canBeCurrentState: false, + linkText: getLinkText(device), + isStateChange: false, + displayed: false, + descriptionText: description, + value: description + ] + def result + def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 2, 0x72: 2, 0x60: 3, 0x33: 2, 0x32: 3, 0x31:2, 0x30: 2, 0x86: 1, 0x7A: 1]) + + if (cmd) { + if ( cmd.CMD != "7006" ) { + result = createEvent(cmd, item1) + } + } + else { + item1.displayed = displayed(description, item1.isStateChange) + result = [item1] + } + //log.debug "Parse returned ${result?.descriptionText}" + result +} + +def getDeviceData() { + def cmd = [] + + cmd << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmd << response(zwave.versionV1.versionGet()) + cmd << response(zwave.firmwareUpdateMdV1.firmwareMdGet()) + + delayBetween(cmd, 500) +} + +def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) { + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) { + updateDataValue("applicationVersion", "${cmd.applicationVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def createEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd, Map item1) { + log.debug "checksum: ${cmd.checksum}" + log.debug "firmwareId: ${cmd.firmwareId}" + log.debug "manufacturerId: ${cmd.manufacturerId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.colorcontrolv1.CapabilityReport cmd, Map item1) { + + log.debug "In CapabilityReport" +} + +def createEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, Map item1) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x26: 1, 0x30: 2, 0x32: 2, 0x33: 2]) // can specify command class versions here like in zwave.parse + //log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") + if ((cmd.sourceEndPoint >= 1) && (cmd.sourceEndPoint <= 5)) { // we don't need color report + //don't do anything + } else { + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } + } +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd, Map item1) { + def result = [:] + if ( cmd.sensorType == 4 ) { //power level comming in + result.name = "power" + result.value = cmd.scaledSensorValue + result.descriptionText = "$device.displayName power usage is ${result.value} watt(s)" + result.isStateChange + sendEvent(name: result.name, value: result.value, displayed: false) + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) { + [] +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) { + [response(zwave.basicV1.basicGet())] +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result +} + +def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) { + def result = doCreateEvent(cmd, item1) + result[0].descriptionText = "${item1.linkText} is ${item1.value}" + result[0].handlerName = cmd.value ? "statusOn" : "statusOff" + for (int i = 0; i < result.size(); i++) { + result[i].type = "digital" + } + result +} + +def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { + def result = [item1] + + item1.name = "switch" + item1.value = cmd.value ? "on" : "off" + item1.handlerName = item1.value + item1.descriptionText = "${item1.linkText} was turned ${item1.value}" + item1.canBeCurrentState = true + item1.isStateChange = isStateChange(device, item1.name, item1.value) + item1.displayed = item1.isStateChange + + if (cmd.value >= 5) { + def item2 = new LinkedHashMap(item1) + item2.name = "level" + item2.value = cmd.value as String + item2.unit = "%" + item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" + item2.canBeCurrentState = true + item2.isStateChange = isStateChange(device, item2.name, item2.value) + item2.displayed = false + result << item2 + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd, item1) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" +} +/* +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "Report: $cmd" + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + [name: "indicatorStatus", value: value, display: false] +} +*/ +def createEvent(physicalgraph.zwave.Command cmd, Map map) { + // Handles any Z-Wave commands we aren't interested in + log.debug "UNHANDLED COMMAND $cmd" +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + + +def poll() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + def cmd = [] + cmd << response(zwave.switchMultilevelV1.switchMultilevelGet().format()) + delayBetween(cmd, 500) +} + + /** + * This method will allow the user to update device parameters (behavior) from an app. + * A "Zwave Tweaker" app will be developed as an interface to do this. Or the user can + * write his/her own app to envoke this method. No type or value checking is done to + * compare to what device capability or reaction. It is up to user to read OEM + * documentation prio to envoking this method. + * + *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! + * + * @param List[paramNumber:80,value:10,size:1] + * + * + * @return none + */ +def updateZwaveParam(params) { + if ( params ) { + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + + def cmds = [] + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1500) + } +} + +def test() { + //def value = [:] + //value = [hue: 0, saturation: 100, level: 5] + //value = [red: 255, green: 0, blue: 255, level: 60] + //setColor(value) + + def cmd = [] + + if ( !state.cnt ) { + state.cnt = 6 + } else { + state.cnt = state.cnt + 1 + } + + if ( state.cnt > 10 ) + state.cnt = 6 + + // run programmed light show + cmd << zwave.configurationV1.configurationSet(configurationValue: [state.cnt], parameterNumber: 72, size: 1).format() + cmd << zwave.configurationV1.configurationGet(parameterNumber: 72).format() + + delayBetween(cmd, 500) + +} + +def colorNameToRgb(color) { + + final colors = [ + [name:"Soft White", r: 255, g: 241, b: 224 ], + [name:"Daylight", r: 255, g: 255, b: 251 ], + [name:"Warm White", r: 255, g: 244, b: 229 ], + + [name:"Red", r: 255, g: 0, b: 0 ], + [name:"Green", r: 0, g: 255, b: 0 ], + [name:"Blue", r: 0, g: 0, b: 255 ], + + [name:"Cyan", r: 0, g: 255, b: 255 ], + [name:"Magenta", r: 255, g: 0, b: 33 ], + [name:"Orange", r: 255, g: 102, b: 0 ], + + [name:"Purple", r: 170, g: 0, b: 255 ], + [name:"Yellow", r: 255, g: 255, b: 0 ], + [name:"White", r: 255, g: 255, b: 255 ] + ] + + def colorData = [:] + colorData = colors.find { it.name == color } + + colorData +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +def hexToRgb(colorHex) { + def rrInt = Integer.parseInt(colorHex.substring(1,3),16) + def ggInt = Integer.parseInt(colorHex.substring(3,5),16) + def bbInt = Integer.parseInt(colorHex.substring(5,7),16) + + def colorData = [:] + colorData = [r: rrInt, g: ggInt, b: bbInt] + colorData +} + +def rgbToHex(rgb) { + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" + + hexColor +} + +def hslToRGB(float var_h, float var_s, float var_l) { + float h = var_h / 100 + float s = var_s / 100 + float l = var_l + + def r = 0 + def g = 0 + def b = 0 + + if (s == 0) { + r = l * 255 + g = l * 255 + b = l * 255 + } else { + float var_2 = 0 + if (l < 0.5) { + var_2 = l * (1 + s) + } else { + var_2 = (l + s) - (s * l) + } + + float var_1 = 2 * l - var_2 + + r = 255 * hueToRgb(var_1, var_2, h + (1 / 3)) + g = 255 * hueToRgb(var_1, var_2, h) + b = 255 * hueToRgb(var_1, var_2, h - (1 / 3)) + } + + def rgb = [:] + rgb = [r: r, g: g, b: b] + + rgb +} + +def hueToRgb(v1, v2, vh) { + if (vh < 0) { vh += 1 } + if (vh > 1) { vh -= 1 } + if ((6 * vh) < 1) { return (v1 + (v2 - v1) * 6 * vh) } + if ((2 * vh) < 1) { return (v2) } + if ((3 * vh) < 2) { return (v1 + (v2 - $v1) * ((2 / 3 - vh) * 6)) } + return (v1) +} + +def rgbToHSL(rgb) { + def r = rgb.r / 255 + def g = rgb.g / 255 + def b = rgb.b / 255 + def h = 0 + def s = 0 + def l = 0 + + def var_min = [r,g,b].min() + def var_max = [r,g,b].max() + def del_max = var_max - var_min + + l = (var_max + var_min) / 2 + + if (del_max == 0) { + h = 0 + s = 0 + } else { + if (l < 0.5) { s = del_max / (var_max + var_min) } + else { s = del_max / (2 - var_max - var_min) } + + def del_r = (((var_max - r) / 6) + (del_max / 2)) / del_max + def del_g = (((var_max - g) / 6) + (del_max / 2)) / del_max + def del_b = (((var_max - b) / 6) + (del_max / 2)) / del_max + + if (r == var_max) { h = del_b - del_g } + else if (g == var_max) { h = (1 / 3) + del_r - del_b } + else if (b == var_max) { h = (2 / 3) + del_g - del_r } + + if (h < 0) { h += 1 } + if (h > 1) { h -= 1 } + } + def hsl = [:] + hsl = [h: h * 100, s: s * 100, l: l] + + hsl +} + +def getColorData(colorName) { + log.debug "getColorData: ${colorName}" + + def colorRGB = colorNameToRgb(colorName) + def colorHex = rgbToHex(colorRGB) + def colorHSL = rgbToHSL(colorRGB) + + def colorData = [:] + colorData = [h: colorHSL.h, + s: colorHSL.s, + l: device.latestValue("level"), + r: colorRGB.r, + g: colorRGB.g, + b: colorRGB.b, + rh: hex(colorRGB.r), + gh: hex(colorRGB.g), + bh: hex(colorRGB.b), + hex: colorHex, + alpha: 1] + + colorData +} + +def doColorButton(colorName) { + log.debug "doColorButton: '${colorName}()'" + + if (device.latestValue("switch") == "off") { on() } + + def level = device.latestValue("level") + def maxLevel = hex(99) + + toggleTiles(colorName.toLowerCase().replaceAll("\\s","")) + + if ( colorName == "Fire Place" ) { updateZwaveParam([paramNumber:72, value:6, size:1]) } + else if ( colorName == "Storm" ) { updateZwaveParam([paramNumber:72, value:7, size:1]) } + else if ( colorName == "Deep Fade" ) { updateZwaveParam([paramNumber:72, value:8, size:1]) } + else if ( colorName == "Lite Fade" ) { updateZwaveParam([paramNumber:72, value:9, size:1]) } + else if ( colorName == "Police" ) { updateZwaveParam([paramNumber:72, value:10, size:1]) } + else if ( colorName == "White" ) { String.format("33050400${maxLevel}02${hex(0)}03${hex(0)}04${hex(0)}%02X", 100) } + else if ( colorName == "Daylight" ) { String.format("33050400${maxLevel}02${maxLevel}03${maxLevel}04${maxLevel}%02X", 100) } + else { + def c = getColorData(colorName) + def newValue = ["hue": c.h, "saturation": c.s, "level": level, "red": c.r, "green": c.g, "blue": c.b, "hex": c.hex, "alpha": c.alpha] + setColor(newValue) + def r = hex(c.r * (level/100)) + def g = hex(c.g * (level/100)) + def b = hex(c.b * (level/100)) + def w = hex(0) //to turn off white channel with toggling tiles + sendRGBW(r, g, b, w) + } +} + +def toggleTiles(color) { + state.colorTiles = [] + if ( !state.colorTiles ) { + state.colorTiles = ["softwhite","daylight","warmwhite","red","green","blue","cyan","magenta","orange","purple","yellow","white","fireplace","storm","deepfade","litefade","police"] + } + + def cmds = [] + + state.colorTiles.each({ + if ( it == color ) { + log.debug "Turning ${it} on" + cmds << sendEvent(name: it, value: "on${it}", display: True, descriptionText: "${device.displayName} ${color} is 'ON'", isStateChange: true) + } else { + //log.debug "Turning ${it} off" + cmds << sendEvent(name: it, value: "off${it}", displayed: false) + } + }) + + delayBetween(cmds, 2500) +} + +// rows of buttons +def softwhite() { doColorButton("Soft White") } +def daylight() { doColorButton("Daylight") } +def warmwhite() { doColorButton("Warm White") } + +def red() { doColorButton("Red") } +def green() { doColorButton("Green") } +def blue() { doColorButton("Blue") } + +def cyan() { doColorButton("Cyan") } +def magenta() { doColorButton("Magenta") } +def orange() { doColorButton("Orange") } + +def purple() { doColorButton("Purple") } +def yellow() { doColorButton("Yellow") } +def white() { doColorButton("White") } + +def fireplace() { doColorButton("Fire Place") } +def storm() { doColorButton("Storm") } +def deepfade() { doColorButton("Deep Fade") } + +def litefade() { doColorButton("Lite Fade") } +def police() { doColorButton("Police") } diff --git a/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy b/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy new file mode 100644 index 00000000000..5e61280e449 --- /dev/null +++ b/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy @@ -0,0 +1,82 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Fortrezz Water Valve", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Valve" + capability "Refresh" + capability "Sensor" + + fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70" + fingerprint deviceId: "0x1006", inClusters: "0x25" + } + + // simulator metadata + simulator { + status "close": "command: 2503, payload: FF" + status "open": "command: 2503, payload: 00" + + // reply messages + reply "2001FF": "command: 2503, payload: FF" + reply "200100": "command: 2503, payload: 00" + } + + // tile definitions + tiles { + standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { + state "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing" + state "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening" + state "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e" + state "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "contact" + details(["contact","refresh"]) + } +} + +def parse(String description) { + log.trace description + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + def value = cmd.value ? "closed" : "open" + [name: "contact", value: value, descriptionText: "$device.displayName valve is $value"] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [:] // Handles all Z-Wave commands we aren't interested in +} + +def open() { + zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format() +} + +def close() { + zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format() +} + +def refresh() { + zwave.switchBinaryV1.switchBinaryGet().format() +} diff --git a/devicetypes/smartthings/foscam.src/foscam.groovy b/devicetypes/smartthings/foscam.src/foscam.groovy new file mode 100644 index 00000000000..c509e657c08 --- /dev/null +++ b/devicetypes/smartthings/foscam.src/foscam.groovy @@ -0,0 +1,192 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Foscam + * + * Author: SmartThings + * Date: 2014-02-04 + */ + metadata { + definition (name: "Foscam", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Sensor" + capability "Image Capture" + } + + simulator { + // TODO: define status and reply messages here + } + + //TODO:encrypt these settings and make them required:true + preferences { + input "username", "text", title: "Username", description: "Your Foscam Username", required: false + input "password", "password", title: "Password", description: "Your Foscam Password", required: false + } + + tiles { + standardTile("image", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: true) { + state "default", label: "", action: "", icon: "st.camera.dropcam-centered", backgroundColor: "#FFFFFF" + } + + carouselTile("cameraDetails", "device.image", width: 3, height: 2) { } + + standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { + state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.dropcam", backgroundColor: "#FFFFFF", nextState:"taking" + state "taking", label:'Taking', action: "", icon: "st.camera.dropcam", backgroundColor: "#53a7c0" + state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.dropcam", backgroundColor: "#FFFFFF", nextState:"taking" + } + + /*standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"getDeviceInfo", icon:"st.secondary.refresh" + }*/ + + main "image" + details(["cameraDetails", "take"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + + def map = stringToMap(description) + log.debug map + + def result = [] + + if (map.bucket && map.key) + { //got a s3 pointer + putImageInS3(map) + } + else if (map.headers && map.body) + { //got device info response + + /* + TODO:need to figure out a way to reliable know which end the snapshot should be taken at. + Current theory is that 8xxx series cameras are at /snapshot.cgi and 9xxx series are at /cgi-bin/CGIProxy.fcgi + */ + + def headerString = new String(map.headers.decodeBase64()) + if (headerString.contains("404 Not Found")) { + state.snapshot = "/snapshot.cgi" + } + + if (map.body) { + def bodyString = new String(map.body.decodeBase64()) + def body = new XmlSlurper().parseText(bodyString) + def productName = body?.productName?.text() + if (productName) + { + log.trace "Got Foscam Product Name: $productName" + state.snapshot = "/cgi-bin/CGIProxy.fcgi" + } + } + } + + result +} + +def putImageInS3(map) { + + def s3ObjectContent + + try { + def imageBytes = getS3Object(map.bucket, map.key + ".jpg") + + if(imageBytes) + { + s3ObjectContent = imageBytes.getObjectContent() + def bytes = new ByteArrayInputStream(s3ObjectContent.bytes) + storeImage(getPictureName(), bytes) + } + } + catch(Exception e) { + log.error e + } + finally { + //explicitly close the stream + if (s3ObjectContent) { s3ObjectContent.close() } + } +} + +// handle commands +def take() { + log.debug "Executing 'take'" + //Snapshot uri depends on model number: + //because 8 series uses user and 9 series uses usr - + //try based on port since issuing a GET with usr to 8 series causes it throw 401 until you reauthorize using basic digest authentication + + def host = getHostAddress() + def port = host.split(":")[1] + def path = (port == "80") ? "/snapshot.cgi?user=${getUsername()}&pwd=${getPassword()}" : "/cgi-bin/CGIProxy.fcgi?usr=${getUsername()}&pwd=${getPassword()}&cmd=snapPicture2" + + + def hubAction = new physicalgraph.device.HubAction( + method: "GET", + path: path, + headers: [HOST:host] + ) + hubAction.options = [outputMsgToS3:true] + hubAction +} + +/*def getDeviceInfo() { + log.debug "Executing 'getDeviceInfo'" + def path = "/cgi-bin/CGIProxy.fcgi" + def hubAction = new physicalgraph.device.HubAction( + method: "GET", + path: path, + headers: [HOST:getHostAddress()], + query:[cmd:"getDevInfo", usr:getUsername(), pwd:getPassword()] + ) +}*/ + +//helper methods +private getPictureName() { + def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') + return device.deviceNetworkId + "_$pictureUuid" + ".jpg" +} + +private getUsername() { + settings.username +} + +private getPassword() { + settings.password +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress() { + def parts = device.deviceNetworkId.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +private hashMD5(String somethingToHash) { + java.security.MessageDigest.getInstance("MD5").digest(somethingToHash.getBytes("UTF-8")).encodeHex().toString() +} + +private calcDigestAuth(String method, String uri) { + def HA1 = hashMD5("${getUsername}::${getPassword}") + def HA2 = hashMD5("${method}:${uri}") + def response = hashMD5("${HA1}::::auth:${HA2}") + + 'Digest username="'+ getUsername() + '", realm="", nonce="", uri="'+ uri +'", qop=auth, nc=, cnonce="", response="' + response + '", opaque=""' +} diff --git a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy new file mode 100644 index 00000000000..366e5c6f1bf --- /dev/null +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -0,0 +1,379 @@ +/** + * GE Link Bulb + * + * Copyright 2014 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. + * + * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart, and others + * + ****************************************************************************** + * Changes + ****************************************************************************** + * + * Change 1: 2014-10-10 (wackford) + * Added setLevel event so subscriptions to the event will work + * Change 2: 2014-12-10 (jscgs350 using Sticks18's code and effort!) + * Modified parse section to properly identify bulb status in the app when manually turned on by a physical switch + * Change 3: 2014-12-12 (jscgs350, Sticks18's) + * Modified to ensure dimming was smoother, and added fix for dimming below 7 + * Change 4: 2014-12-14 Part 1 (Sticks18) + * Modified to ignore unnecessary level change responses to prevent level skips + * Change 5: 2014-12-14 Part 2 (Sticks18, jscgs350) + * Modified to clean up trace&debug logging, added new code from @sticks18 for parsing "on/off" to determine if the bulb is manually turned on and immediately update the app + * Change 6: 2015-01-02 (Sticks18) + * Modified to allow dim rate in Preferences. Added ability to dim during On/Off commands and included this option in Preferences. Defaults are "Normal" and no dim for On/Off. + * Change 7: 2015-01-09 (tslagle13) + * dimOnOff is was boolean, and switched to enum. Properly update "rampOn" and "rampOff" when refreshed or a polled (dim transition for On/Off commands) + * Change 8: 2015-03-06 (Juan Risso) + * Slider range from 0..100 + * Change 9: 2015-03-06 (Juan Risso) + * Setlevel -> value to integer (to prevent smartapp calling this function from not working). + * + */ + +metadata { + definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") { + + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" + capability "Polling" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState:"turningOff" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action: "switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action: "switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + main(["switch"]) + details(["switch", "level", "levelSliderControl", "refresh"]) + } + + preferences { + + input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) + input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) + + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.trace description + + if (description?.startsWith("on/off:")) { + log.debug "The bulb was sent a command to do something just now..." + if (description[-1] == "1") { + def result = createEvent(name: "switch", value: "on") + log.debug "On command was sent maybe from manually turning on? : Parse returned ${result?.descriptionText}" + return result + } else if (description[-1] == "0") { + def result = createEvent(name: "switch", value: "off") + log.debug "Off command was sent : Parse returned ${result?.descriptionText}" + return result + } + } + + def msg = zigbee.parse(description) + + if (description?.startsWith("catchall:")) { + // log.trace msg + // log.trace "data: $msg.data" + + def x = description[-4..-1] + // log.debug x + + switch (x) + { + + case "0000": + + def result = createEvent(name: "switch", value: "off") + log.debug "${result?.descriptionText}" + return result + break + + case "1000": + + def result = createEvent(name: "switch", value: "off") + log.debug "${result?.descriptionText}" + return result + break + + case "0100": + + def result = createEvent(name: "switch", value: "on") + log.debug "${result?.descriptionText}" + return result + break + + case "1001": + + def result = createEvent(name: "switch", value: "on") + log.debug "${result?.descriptionText}" + return result + break + } + } + + if (description?.startsWith("read attr")) { + + // log.trace description[27..28] + // log.trace description[-2..-1] + + if (description[27..28] == "0A") { + + // log.debug description[-2..-1] + def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) + sendEvent( name: "level", value: i ) + sendEvent( name: "switch.setLevel", value: i) //added to help subscribers + + } + + else { + + if (description[-2..-1] == "00" && state.trigger == "setLevel") { + // log.debug description[-2..-1] + def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) + sendEvent( name: "level", value: i ) + sendEvent( name: "switch.setLevel", value: i) //added to help subscribers + } + + if (description[-2..-1] == state.lvl) { + // log.debug description[-2..-1] + def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) + sendEvent( name: "level", value: i ) + sendEvent( name: "switch.setLevel", value: i) //added to help subscribers + } + + } + } + +} + +def poll() { + + [ + "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}" + ] + +} + +def updated() { + + state.dOnOff = "0000" + + if (dimRate) { + + switch (dimRate) + { + + case "Instant": + + state.rate = "0000" + if (dimOnOff) { state.dOnOff = "0000"} + break + + case "Normal": + + state.rate = "1500" + if (dimOnOff) { state.dOnOff = "0015"} + break + + case "Slow": + + state.rate = "2500" + if (dimOnOff) { state.dOnOff = "0025"} + break + + case "Very Slow": + + state.rate = "3500" + if (dimOnOff) { state.dOnOff = "0035"} + break + + } + + } + + else { + + state.rate = "1500" + state.dOnOff = "0000" + + } + + if (dimOnOff == "Yes"){ + switch (dimOnOff){ + case "InstantOnOff": + + state.rate = "0000" + if (state.rate == "0000") { state.dOnOff = "0000"} + break + + case "NormalOnOff": + + state.rate = "1500" + if (state.rate == "1500") { state.dOnOff = "0015"} + break + + case "SlowOnOff": + + state.rate = "2500" + if (state.rate == "2500") { state.dOnOff = "0025"} + break + + case "Very SlowOnOff": + + state.rate = "3500" + if (state.rate == "3500") { state.dOnOff = "0035"} + break + + } + + } + else{ + state.dOnOff = "0000" + } + + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}" + + +} + +def on() { + state.lvl = "00" + state.trigger = "on/off" + + // log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} + +def off() { + state.lvl = "00" + state.trigger = "on/off" + + // log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} + +def refresh() { + + [ + "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}" + ] + poll() + +} + +def setLevel(value) { + + def cmds = [] + value = value as Integer + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {0000 ${state.rate}}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: value) + value = (value * 255 / 100) + def level = hex(value); + + state.trigger = "setLevel" + state.lvl = "${level}" + + if (dimRate) { + cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}" + } + else { + cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 1500}" + } + + log.debug cmds + cmds +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting and Bindings." + def configCmds = [ + + //Switch Reporting + "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 1000", + + //Level Control Reporting + "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} \ No newline at end of file diff --git a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy new file mode 100644 index 00000000000..3a103811ffe --- /dev/null +++ b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy @@ -0,0 +1,352 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * GE/Jasco ZigBee Dimmer + * + * Author: SmartThings + * Date: 2015-07-01 + */ + +metadata { + definition (name: "GE ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Switch Level" + capability "Power Meter" + capability "Configuration" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + valueTile("power", "device.power", decoration: "flat") { + state "power", label:'${currentValue} W' + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "level", "power","levelSliderControl","refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def finalResult = isKnownDescription(description) + if (finalResult != "false") { + log.info finalResult + if (finalResult.type == "update") { + log.info "$device updates: ${finalResult.value}" + } + else if (finalResult.type == "power") { + def powerValue = (finalResult.value as Integer)/10 + sendEvent(name: "power", value: powerValue) + + /* + Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 + + power level is an integer. The exact power level with correct units needs to be handled in the device type + to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 + */ + } + else { + sendEvent(name: finalResult.type, value: finalResult.value) + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug parseDescriptionAsMap(description) + } +} + +// Commands to device +def zigbeeCommand(cluster, attribute){ + ["st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"] +} + +def off() { + zigbeeCommand("6", "0") +} + +def on() { + zigbeeCommand("6", "1") +} + +def setLevel(value) { + value = value as Integer + if (value == 0) { + off() + } + else { + sendEvent(name: "level", value: value) + setLevelWithRate(value, "0000") + ["delay 1000"] + on() //value is between 0 to 100; GE does NOT switch on if OFF + } +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500" + ] + +} + +def configure() { + onOffConfig() + levelConfig() + powerConfig() + refresh() +} + + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +//Need to reverse array of size 2 +private byte[] reverseArray(byte[] array) { + byte tmp; + tmp = array[1]; + array[1] = array[0]; + array[0] = tmp; + return array +} + +def parseDescriptionAsMap(description) { + if (description?.startsWith("read attr -")) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] + } + } + else if (description?.startsWith("catchall: ")) { + def seg = (description - "catchall: ").split(" ") + def zigbeeMap = [:] + zigbeeMap += [raw: (description - "catchall: ")] + zigbeeMap += [profileId: seg[0]] + zigbeeMap += [clusterId: seg[1]] + zigbeeMap += [sourceEndpoint: seg[2]] + zigbeeMap += [destinationEndpoint: seg[3]] + zigbeeMap += [options: seg[4]] + zigbeeMap += [messageType: seg[5]] + zigbeeMap += [dni: seg[6]] + zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] + zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] + zigbeeMap += [manufacturerId: seg[9]] + zigbeeMap += [command: seg[10]] + zigbeeMap += [direction: seg[11]] + zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { + it.join('') + } : []] + + zigbeeMap + } +} + +def isKnownDescription(description) { + if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { + def descMap = parseDescriptionAsMap(description) + if (descMap.cluster == "0006" || descMap.clusterId == "0006") { + isDescriptionOnOff(descMap) + } + else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){ + isDescriptionLevel(descMap) + } + else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){ + isDescriptionPower(descMap) + } + else { + return "false" + } + } + else if(description?.startsWith("on/off:")) { + def switchValue = description?.endsWith("1") ? "on" : "off" + return [type: "switch", value : switchValue] + } + else { + return "false" + } +} + +def isDescriptionOnOff(descMap) { + def switchValue = "undefined" + if (descMap.cluster == "0006") { //cluster info from read attr + value = descMap.value + if (value == "01"){ + switchValue = "on" + } + else if (value == "00"){ + switchValue = "off" + } + } + else if (descMap.clusterId == "0006") { + //cluster info from catch all + //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 + //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 + if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ + switchValue = "on" + } + else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ + switchValue = "off" + } + else if(descMap.command=="07"){ + return [type: "update", value : "switch (0006) capability configured successfully"] + } + } + + if (switchValue != "undefined"){ + return [type: "switch", value : switchValue] + } + else { + return "false" + } + +} + +//@return - false or "success" or level [0-100] +def isDescriptionLevel(descMap) { + def dimmerValue = -1 + if (descMap.cluster == "0008"){ + //TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message + def value = convertHexToInt(descMap.value) + dimmerValue = Math.round(value * 100 / 255) + if(dimmerValue==0 && value > 0) { + dimmerValue = 1 //handling for non-zero hex value less than 3 + } + } + else if(descMap.clusterId == "0008") { + if(descMap.command=="0B"){ + return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent. + } + else if(descMap.command=="07"){ + return [type: "update", value : "level (0008) capability configured successfully"] + } + } + + if (dimmerValue != -1){ + return [type: "level", value : dimmerValue] + + } + else { + return "false" + } +} + +def isDescriptionPower(descMap) { + def powerValue = "undefined" + if (descMap.cluster == "0702") { + if (descMap.attrId == "0400") { + powerValue = convertHexToInt(descMap.value) + } + } + else if (descMap.clusterId == "0702") { + if(descMap.command=="07"){ + return [type: "update", value : "power (0702) capability configured successfully"] + } + } + + if (powerValue != "undefined"){ + return [type: "power", value : powerValue] + } + else { + return "false" + } +} + + +def onOffConfig() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 6 0 0x10 0 600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s) +//min level change is 01 +def levelConfig() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 8 0 0x20 1 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) +//min change in value is 05 +def powerConfig() { + [ + //Meter (Power) Reporting + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +def setLevelWithRate(level, rate) { + if(rate == null){ + rate = "0000" + } + level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex + ["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"] +} + +String convertToHexString(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/ge-zigbee-switch.src/ge-zigbee-switch.groovy b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy new file mode 100644 index 00000000000..ea0ce0b75d6 --- /dev/null +++ b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy @@ -0,0 +1,285 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * GE/Jasco ZigBee Switch + * + * Author: SmartThings + * Date: 2015-07-01 + */ + +metadata { + // Automatically generated. Make future change here. + definition (name: "GE ZigBee Switch", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Power Meter" + capability "Configuration" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("power", "device.power", inactiveLabel: false, decoration: "flat") { + state "power", label:'${currentValue} Watts' + } + main "switch" + details(["switch", "power", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def finalResult = isKnownDescription(description) + if (finalResult != "false") { + log.info finalResult + if (finalResult.type == "update") { + log.info "$device updates: ${finalResult.value}" + } + else if (finalResult.type == "power") { + def powerValue = (finalResult.value as Integer)/10 + sendEvent(name: "power", value: powerValue) + + /* + Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 + + power level is an integer. The exact power level with correct units needs to be handled in the device type + to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 + */ + } + else { + sendEvent(name: finalResult.type, value: finalResult.value) + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug parseDescriptionAsMap(description) + } +} + +// Commands to device +def zigbeeCommand(cluster, attribute){ + "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" +} + +def off() { + zigbeeCommand("6", "0") +} + +def on() { + zigbeeCommand("6", "1") +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500" + ] + +} + +def configure() { + onOffConfig() + powerConfig() + refresh() +} + + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +//Need to reverse array of size 2 +private byte[] reverseArray(byte[] array) { + byte tmp; + tmp = array[1]; + array[1] = array[0]; + array[0] = tmp; + return array +} + +def parseDescriptionAsMap(description) { + if (description?.startsWith("read attr -")) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] + } + } + else if (description?.startsWith("catchall: ")) { + def seg = (description - "catchall: ").split(" ") + def zigbeeMap = [:] + zigbeeMap += [raw: (description - "catchall: ")] + zigbeeMap += [profileId: seg[0]] + zigbeeMap += [clusterId: seg[1]] + zigbeeMap += [sourceEndpoint: seg[2]] + zigbeeMap += [destinationEndpoint: seg[3]] + zigbeeMap += [options: seg[4]] + zigbeeMap += [messageType: seg[5]] + zigbeeMap += [dni: seg[6]] + zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] + zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] + zigbeeMap += [manufacturerId: seg[9]] + zigbeeMap += [command: seg[10]] + zigbeeMap += [direction: seg[11]] + zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { + it.join('') + } : []] + + zigbeeMap + } +} + +def isKnownDescription(description) { + if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { + def descMap = parseDescriptionAsMap(description) + if (descMap.cluster == "0006" || descMap.clusterId == "0006") { + isDescriptionOnOff(descMap) + } + else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){ + isDescriptionPower(descMap) + } + else { + return "false" + } + } + else if(description?.startsWith("on/off:")) { + def switchValue = description?.endsWith("1") ? "on" : "off" + return [type: "switch", value : switchValue] + } + else { + return "false" + } +} + +def isDescriptionOnOff(descMap) { + def switchValue = "undefined" + if (descMap.cluster == "0006") { //cluster info from read attr + value = descMap.value + if (value == "01"){ + switchValue = "on" + } + else if (value == "00"){ + switchValue = "off" + } + } + else if (descMap.clusterId == "0006") { + //cluster info from catch all + //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 + //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 + if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ + switchValue = "on" + } + else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ + switchValue = "off" + } + else if(descMap.command=="07"){ + return [type: "update", value : "switch (0006) capability configured successfully"] + } + } + + if (switchValue != "undefined"){ + return [type: "switch", value : switchValue] + } + else { + return "false" + } + +} + +def isDescriptionPower(descMap) { + def powerValue = "undefined" + if (descMap.cluster == "0702") { + if (descMap.attrId == "0400") { + powerValue = convertHexToInt(descMap.value) + } + } + else if (descMap.clusterId == "0702") { + if(descMap.command=="07"){ + return [type: "update", value : "power (0702) capability configured successfully"] + } + } + + if (powerValue != "undefined"){ + return [type: "power", value : powerValue] + } + else { + return "false" + } +} + + +def onOffConfig() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 6 0 0x10 0 600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) +//min change in value is 05 +def powerConfig() { + [ + //Meter (Power) Reporting + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +String convertToHexString(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/home-energy-meter.src/home-energy-meter.groovy b/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy new file mode 100644 index 00000000000..27db465f0df --- /dev/null +++ b/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy @@ -0,0 +1,106 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Home Energy Meter", namespace: "smartthings", author: "SmartThings") { + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Polling" + capability "Sensor" + + command "reset" + + fingerprint deviceId: "0x3103", inClusters: "0x32" + fingerprint inClusters: "0x32" + } + + // simulator metadata + simulator { + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + } + + // tile definitions + tiles { + valueTile("power", "device.power", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["power","energy"]) + details(["power","energy", "reset", "refresh"]) + } +} + +def updated() { + response(refresh()) +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + log.debug "Parsed '$description' to $result" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { + if (cmd.scale == 0) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } + else { + [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "$device.displayName: $cmd" + [:] +} + +def refresh() { + delayBetween([ + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def poll() { + refresh() +} + +def reset() { + delayBetween([ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ], 1000) +} diff --git a/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy b/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy new file mode 100644 index 00000000000..b6d495e23f0 --- /dev/null +++ b/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy @@ -0,0 +1,200 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "HomeSeer Multisensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Configuration" + capability "Illuminance Measurement" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: "0x2101", inClusters: "0x60,0x31,0x70,0x84,0x85,0x80,0x72,0x77,0x86" + } + + simulator { + // messages the device returns in response to commands it receives + status "motion (basic)" : "command: 2001, payload: FF" + status "no motion (basic)" : "command: 2001, payload: 00" + status "76.7 deg F" : "command: 6006, payload: 03 31 05 01 2A 02 FF" + status "80.6 deg F" : "command: 6006, payload: 03 31 05 01 2A 03 26" + status "2 lux" : "command: 6006, payload: 02 31 05 03 01 02" + status "80 lux" : "command: 6006, payload: 02 31 05 03 01 50" + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main(["motion", "temperature", "illuminance"]) + details(["motion", "temperature", "illuminance", "battery"]) + } + + preferences { + input "intervalMins", "number", title: "Multisensor report (minutes)", description: "Minutes between temperature/illuminance readings", defaultValue: 20, required: false, displayDuringSetup: true + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + // log.debug "Parsed ${description.inspect()} to ${result.inspect()}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.multiinstancev1.MultiInstanceCmdEncap cmd) { + def encapsulated = null + if (cmd.respondsTo("encapsulatedCommand")) { + encapsulated = cmd.encapsulatedCommand() + } else { + def hex1 = { n -> String.format("%02X", n) } + def sorry = "command: ${hex1(cmd.commandClass)}${hex1(cmd.command)}, payload: " + cmd.parameter.collect{ hex1(it) }.join(" ") + encapsulated = zwave.parse(sorry, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1]) + } + return encapsulated ? zwaveEvent(encapsulated) : null +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def results = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + + if (state.config) { + state.config = false + results << response(configure()) + } + def prevBattery = device.currentState("battery") + if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) { + results << response(zwave.batteryV1.batteryGet().format()) + } + results << response(temperatureGetCmd().format()) + results << response(illuminanceGetCmd().format()) + + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv1.SensorMultilevelReport cmd) +{ + def map = [:] + switch (cmd.sensorType) { + case 1: + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + break; + case 3: + // luminance + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + map.name = "illuminance" + break; + case 5: + // humidity + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "%" + map.name = "humidity" + break; + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery!" + } else { + map.value = cmd.batteryLevel + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + def map = [:] + map.value = cmd.sensorValue ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected motion" + } + else { + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + def map = [:] + map.value = cmd.value ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "$device.displayName detected motion" + } + else { + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def temperatureGetCmd() { + zwave.multiInstanceV1.multiInstanceCmdEncap(instance:3, commandClass:0x31, command:0x04) +} + +def illuminanceGetCmd() { + zwave.multiInstanceV1.multiInstanceCmdEncap(instance:2, commandClass:0x31, command:0x04) +} + +def updated() { + log.debug "Will configure wakeup interval (${60 * (settings.intervalMins ?: 20).toInteger()} seconds)" + state.config = true +} + +def configure() { + zwave.wakeUpV2.wakeUpIntervalSet(seconds: 60 * (settings.intervalMins ?: 20).toInteger(), nodeid: zwaveHubNodeId).format() +} diff --git a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy new file mode 100644 index 00000000000..37370bb4aaa --- /dev/null +++ b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy @@ -0,0 +1,78 @@ +/** + * Hue Bridge + * + * Author: SmartThings + */ +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") { + attribute "serialNumber", "string" + attribute "networkAddress", "string" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { + state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF" + } + valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) { + state "default", label:'SN: ${currentValue}' + } + valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) { + state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false + } + + main (["icon"]) + details(["networkAddress","serialNumber"]) + } +} + +// parse events into attributes +def parse(description) { + log.debug "Parsing '${description}'" + def results = [] + def result = parent.parse(this, description) + + if (result instanceof physicalgraph.device.HubAction){ + log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?" + results << result + } else if (description == "updated") { + //do nothing + log.trace "HUE BRIDGE was updated" + } else { + log.trace "HUE BRIDGE, OTHER" + def map = description + if (description instanceof String) { + map = stringToMap(description) + } + if (map?.name && map?.value) { + log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value" + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + else { + log.trace "HUE BRIDGE, OTHER" + def msg = parseLanMessage(description) + if (msg.body) { + def contentType = msg.headers["Content-Type"] + if (contentType?.contains("json")) { + def bulbs = new groovy.json.JsonSlurper().parseText(msg.body) + if (bulbs.state) { + log.warn "NOT PROCESSED: $msg.body" + } + else { + log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs" + sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false) + } + } + else if (contentType?.contains("xml")) { + log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT" + } + } + } + } + results +} diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy new file mode 100644 index 00000000000..e1ecb30775e --- /dev/null +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -0,0 +1,166 @@ +/** + * Hue Bulb + * + * Author: SmartThings + */ +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Switch" + capability "Refresh" + capability "Sensor" + + command "setAdjustedColor" + command "reset" + command "refresh" + } + + simulator { + // TODO: define status and reply messages here + } + + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821" + state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff" + } + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") { + state "default", label:"Color Reset", action:"reset", icon:"st.lights.philips.hue-single" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setAdjustedColor" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { + state "saturation", action:"color control.setSaturation" + } + valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { + state "saturation", label: 'Sat ${currentValue} ' + } + controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { + state "hue", action:"color control.setHue" + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } + + main(["switch"]) + details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"]) + +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + def map = description + if (description instanceof String) { + log.debug "Hue Bulb stringToMap - ${map}" + map = stringToMap(description) + } + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +def on(transition = "4") { + log.trace parent.on(this,transition) + sendEvent(name: "switch", value: "on") +} + +def off(transition = "4") { + log.trace parent.off(this,transition) + sendEvent(name: "switch", value: "off") +} + +def nextLevel() { + def level = device.latestValue("level") as Integer ?: 0 + if (level <= 100) { + level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer + } + else { + level = 25 + } + setLevel(level) +} + +def setLevel(percent) { + log.debug "Executing 'setLevel'" + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent) +} + +def setSaturation(percent) { + log.debug "Executing 'setSaturation'" + parent.setSaturation(this, percent) + sendEvent(name: "saturation", value: percent) +} + +def setHue(percent) { + log.debug "Executing 'setHue'" + parent.setHue(this, percent) + sendEvent(name: "hue", value: percent) +} + +def setColor(value,alert = "none",transition = 4) { + log.debug "setColor: ${value}, $this" + parent.setColor(this, value, alert, transition) + if (value.hue) { sendEvent(name: "hue", value: value.hue)} + if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} + if (value.hex) { sendEvent(name: "color", value: value.hex)} + if (value.level) { sendEvent(name: "level", value: value.level)} + if (value.switch) { sendEvent(name: "switch", value: value.switch)} +} + +def reset() { + log.debug "Executing 'reset'" + def value = [level:100, hex:"#90C638", saturation:56, hue:23] + setAdjustedColor(value) + parent.poll() +} + +def setAdjustedColor(value) { + if (value) { + log.trace "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.hue = adjustOutgoingHue(value.hue) + // Needed because color picker always sends 100 + adjusted.level = null + setColor(adjusted) + } +} + +def refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} + +def adjustOutgoingHue(percent) { + def adjusted = percent + if (percent > 31) { + if (percent < 63.0) { + adjusted = percent + (7 * (percent -30 ) / 32) + } + else if (percent < 73.0) { + adjusted = 69 + (5 * (percent - 62) / 10) + } + else { + adjusted = percent + (2 * (100 - percent) / 28) + } + } + log.info "percent: $percent, adjusted: $adjusted" + adjusted +} diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy new file mode 100644 index 00000000000..5fe1e5f1dc8 --- /dev/null +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -0,0 +1,79 @@ +/** + * Hue Lux Bulb + * + * Author: SmartThings + */ +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Sensor" + + command "refresh" + } + + simulator { + // TODO: define status and reply messages here + } + + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821" + state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + main(["switch"]) + details(["switch", "levelSliderControl", "refresh"]) + +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + def map = description + if (description instanceof String) { + log.debug "Hue Bulb stringToMap - ${map}" + map = stringToMap(description) + } + + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +def on() { + parent.on(this) + sendEvent(name: "switch", value: "on") +} + +def off() { + parent.off(this) + sendEvent(name: "switch", value: "off") +} + +def setLevel(percent) { + log.debug "Executing 'setLevel'" + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent) +} + +def refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} diff --git a/devicetypes/smartthings/life360-user.src/life360-user.groovy b/devicetypes/smartthings/life360-user.src/life360-user.groovy new file mode 100644 index 00000000000..d78f5c3bd42 --- /dev/null +++ b/devicetypes/smartthings/life360-user.src/life360-user.groovy @@ -0,0 +1,91 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Life360-User + * + * Author: jeff + * Date: 2013-08-15 + */ + +metadata { + definition (name: "Life360 User", namespace: "smartthings", author: "SmartThings") { + capability "Presence Sensor" + capability "Sensor" + } + + simulator { + status "present": "presence: 1" + status "not present": "presence: 0" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") + state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") + } + + main "presence" + details "presence" + } +} + +def generatePresenceEvent(boolean present) { + log.debug "Here in generatePresenceEvent!" + def value = formatValue(present) + def linkText = getLinkText(device) + def descriptionText = formatDescriptionText(linkText, present) + def handlerName = getState(present) + + def results = [ + name: "presence", + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName + ] + log.debug "Generating Event: ${results}" + sendEvent (results) +} + +def setMemberId (String memberId) { + log.debug "MemberId = ${memberId}" + state.life360MemberId = memberId +} + +def getMemberId () { + + log.debug "MemberId = ${state.life360MemberId}" + + return(state.life360MemberId) +} + +private String formatValue(boolean present) { + if (present) + return "present" + else + return "not present" +} + +private formatDescriptionText(String linkText, boolean present) { + if (present) + return "Life360 User $linkText has arrived" + else + return "Life360 User $linkText has left" +} + +private getState(boolean present) { + if (present) + return "arrived" + else + return "left" +} diff --git a/devicetypes/smartthings/light-sensor.src/light-sensor.groovy b/devicetypes/smartthings/light-sensor.src/light-sensor.groovy new file mode 100644 index 00000000000..6acd58140d4 --- /dev/null +++ b/devicetypes/smartthings/light-sensor.src/light-sensor.groovy @@ -0,0 +1,61 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Light Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Illuminance Measurement" + capability "Sensor" + + fingerprint profileId: "0104", deviceId: "0106", inClusters: "0000,0001,0003,0009,0400" + } + + // simulator metadata + simulator { + status "dark": "illuminance: 8" + status "light": "illuminance: 300" + status "bright": "illuminance: 1000" + } + + // UI tile definitions + tiles { + valueTile("illuminance", "device.illuminance", width: 2, height: 2) { + state("illuminance", label:'${currentValue}', unit:"lux", + backgroundColors:[ + [value: 9, color: "#767676"], + [value: 315, color: "#ffa81e"], + [value: 1000, color: "#fbd41b"] + ] + ) + } + + main "illuminance" + details "illuminance" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def result + if (description?.startsWith("illuminance: ")) { + def raw = description - "illuminance: " + if (raw.isNumber()) { + result = createEvent( + name: "illuminance", + value: Math.round(zigbee.lux(raw as Integer)).toString(), + unit: "lux" + ) + } + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} diff --git a/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy b/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy new file mode 100644 index 00000000000..8395f80b3a5 --- /dev/null +++ b/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy @@ -0,0 +1,64 @@ +/** + * Logitech Harmony Hub + * + * Author: SmartThings + */ +metadata { + definition (name: "Logitech Harmony Hub C2C", namespace: "smartthings", author: "SmartThings") { + capability "Media Controller" + capability "Refresh" + + command "activityoff" + command "alloff" + command "refresh" + } + + simulator { + } + + tiles { + standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { + state "default", label: "Harmony", action: "", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#FFFFFF" + } + valueTile("currentActivity", "device.currentActivity", decoration: "flat", height: 1, width: 3, inactiveLabel: false) { + state "default", label:'${currentValue}' + } + standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'End Activity', action:"activityoff", icon:"st.harmony.harmony-hub-icon" + } + standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'All Activities', action:"alloff", icon:"st.secondary.off" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["icon"]) + details(["currentActivity", "huboff", "refresh"]) + } +} + +def startActivity(String activityId) { + log.debug "Executing 'Start Activity'" + log.trace parent.activity("$device.deviceNetworkId-$activityId","start") +} + +def activityoff() { + log.debug "Executing 'Activity Off'" + log.trace parent.activity(device.deviceNetworkId,"hub") +} + +def alloff() { + log.debug "Executing 'All Off'" + log.trace parent.activity("all","end") +} + +def poll() { + log.debug "Executing 'Poll'" + log.trace parent.poll() +} + +def refresh() { + log.debug "Executing 'Refresh'" + log.trace parent.poll() +} diff --git a/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy b/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy new file mode 100644 index 00000000000..fb7407ed310 --- /dev/null +++ b/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy @@ -0,0 +1,175 @@ +/** + * MimoLite Garage Door Controller + * + * Copyright 2014 Todd Wackford + * + * 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. + * + * Note: This device type is based on the work of Jit Jack (cesaldar) as posted on the SmartThings Community website. + * + * This device type file will configure a Fortrezz MimoLite Wireless Interface/Bridge Module as a Garage Door + * Controller. The Garage Door must be physically configured per the following diagram: + * "http://www.fortrezz.com/index.php/component/jdownloads/finish/4/17?Itemid=0" + * for all functionality to work correctly. + * + * This device type will also set the atttibute "powered" to "powerOn" or "powerOff" accordingly. This uses + * the alarm capability of the MimoLite and the status will be displayed to the user on a secondary tile. User + * can subscribe to the status of this atttribute to be notified when power drops out. + * + * This device type implements a "Configure" action tile which will set the momentary switch timeout to 25ms and + * turn on the powerout alarm. + * + * + */ +metadata { + // Automatically generated. Make future change here. + definition (name: "MimoLite Garage Door Controller", namespace: "smartthings", author: "Todd Wackford") { + capability "Configuration" + capability "Polling" + capability "Switch" + capability "Refresh" + capability "Contact Sensor" + + attribute "powered", "string" + + command "on" + command "off" + + fingerprint deviceId: "0x1000", inClusters: "0x72,0x86,0x71,0x30,0x31,0x35,0x70,0x85,0x25,0x03" + } + + simulator { + // Simulator stuff + + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "doorClosed", label: "Closed", action: "on", icon: "st.doors.garage.garage-closed", backgroundColor: "#79b821" + state "doorOpen", label: "Open", action: "on", icon: "st.doors.garage.garage-open", backgroundColor: "#ffa81e" + state "doorOpening", label: "Opening", action: "on", icon: "st.doors.garage.garage-opening", backgroundColor: "#ffa81e" + state "doorClosing", label: "Closing", action: "on", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffa81e" + state "on", label: "Actuate", action: "off", icon: "st.doors.garage.garage-closed", backgroundColor: "#53a7c0" + state "off", label: '${name}', action: "on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("contact", "device.contact", inactiveLabel: false) { + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("powered", "device.powered", inactiveLabel: false) { + state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + main (["switch", "contact"]) + details(["switch", "powered", "refresh", "configure"]) + } +} + +def parse(String description) { +log.debug "description is: ${description}" + + def result = null + def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x30: 1, 0x70: 1]) + + log.debug "command value is: $cmd.CMD" + + if (cmd.CMD == "7105") { //Mimo sent a power loss report + log.debug "Device lost power" + sendEvent(name: "powered", value: "powerOff", descriptionText: "$device.displayName lost power") + } else { + sendEvent(name: "powered", value: "powerOn", descriptionText: "$device.displayName regained power") + } + + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def sensorValueEvent(Short value) { + if (value) { + sendEvent(name: "contact", value: "open") + sendEvent(name: "switch", value: "doorOpen") + } else { + sendEvent(name: "contact", value: "closed") + sendEvent(name: "switch", value: "doorClosed") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + def doorState = device.currentValue('contact') + if ( doorState == "closed") + [name: "switch", value: cmd.value ? "on" : "doorOpening", type: "digital"] + else + [name: "switch", value: cmd.value ? "on" : "doorClosing", type: "digital"] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) +{ + log.debug "We lost power" //we caught this up in the parse method. This method not used. +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def configure() { + log.debug "Configuring...." //setting up to monitor power alarm and actuator duration + delayBetween([ + zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(), + zwave.configurationV1.configurationSet(configurationValue: [25], parameterNumber: 11, size: 1).format(), + zwave.configurationV1.configurationGet(parameterNumber: 11).format() + ]) +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def poll() { + zwave.switchBinaryV1.switchBinaryGet().format() +} + +def refresh() { + zwave.switchBinaryV1.switchBinaryGet().format() +} diff --git a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy new file mode 100644 index 00000000000..99746f2069d --- /dev/null +++ b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy @@ -0,0 +1,87 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Mobile Presence", namespace: "smartthings", author: "SmartThings") { + capability "Presence Sensor" + capability "Sensor" + } + + simulator { + status "present": "presence: 1" + status "not present": "presence: 0" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") + state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") + } + main "presence" + details "presence" + } +} + +def parse(String description) { + def name = parseName(description) + def value = parseValue(description) + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def handlerName = getState(value) + def isStateChange = isStateChange(device, name, value) + + def results = [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] + log.debug "Parse returned $results.descriptionText" + return results + +} + +private String parseName(String description) { + if (description?.startsWith("presence: ")) { + return "presence" + } + null +} + +private String parseValue(String description) { + switch(description) { + case "presence: 1": return "present" + case "presence: 0": return "not present" + default: return description + } +} + +private parseDescriptionText(String linkText, String value, String description) { + switch(value) { + case "present": return "$linkText has arrived" + case "not present": return "$linkText has left" + default: return value + } +} + +private getState(String value) { + switch(value) { + case "present": return "arrived" + case "not present": return "left" + default: return value + } +} diff --git a/devicetypes/smartthings/momentary-button-tile.src/momentary-button-tile.groovy b/devicetypes/smartthings/momentary-button-tile.src/momentary-button-tile.groovy new file mode 100644 index 00000000000..b9684575ab6 --- /dev/null +++ b/devicetypes/smartthings/momentary-button-tile.src/momentary-button-tile.groovy @@ -0,0 +1,57 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Momentary Button Tile + * + * Author: SmartThings + * + * Date: 2013-05-01 + */ +metadata { + definition (name: "Momentary Button Tile", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Momentary" + capability "Sensor" + } + + // simulator metadata + simulator { + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: 'Push', action: "momentary.push", backgroundColor: "#ffffff", nextState: "on" + state "on", label: 'Push', action: "momentary.push", backgroundColor: "#53a7c0" + } + main "switch" + details "switch" + } +} + +def parse(String description) { +} + +def push() { + sendEvent(name: "switch", value: "on", isStateChange: true, display: false) + sendEvent(name: "switch", value: "off", isStateChange: true, display: false) + sendEvent(name: "momentary", value: "pushed", isStateChange: true) +} + +def on() { + push() +} + +def off() { + push() +} diff --git a/devicetypes/smartthings/motion-detector.src/motion-detector.groovy b/devicetypes/smartthings/motion-detector.src/motion-detector.groovy new file mode 100644 index 00000000000..a7c86a467e0 --- /dev/null +++ b/devicetypes/smartthings/motion-detector.src/motion-detector.groovy @@ -0,0 +1,60 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Motion Detector", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Sensor" + + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500" + } + + // simulator metadata + simulator { + status "active": "zone report :: type: 19 value: 0031" + status "inactive": "zone report :: type: 19 value: 0030" + } + + // UI tile definitions + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + } + + main "motion" + details "motion" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def name = null + def value = description + def descriptionText = null + if (zigbee.isZoneType19(description)) { + name = "motion" + def isActive = zigbee.translateStatusZoneType19(description) + value = isActive ? "active" : "inactive" + descriptionText = isActive ? "${device.displayName} detected motion" : "${device.displayName} motion has stopped" + } + + def result = createEvent( + name: name, + value: value, + descriptionText: descriptionText + ) + + log.debug "Parse returned ${result?.descriptionText}" + return result +} diff --git a/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy b/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy new file mode 100644 index 00000000000..40a76f34f30 --- /dev/null +++ b/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy @@ -0,0 +1,251 @@ +/** + * NYCE Motion Sensor + * + * Copyright 2014 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: "NYCE Motion Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Refresh" + + command "enrollResponse" + + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041" + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043" + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045" + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery' + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["motion"]) + details(["motion","battery","refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = getBatteryPercentage(cluster.data.last()) + break + + case 0x0406: + log.debug 'motion' + resultMap.name = 'motion' + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private int getBatteryPercentage(int value) { + def minVolts = 2.1 + def maxVolts = 3.0 + def volts = value / 10 + def pct = (volts - minVolts) / (maxVolts - minVolts) + if(pct>1) + pct=1 //if battery is overrated, decreasing battery value to 100% + return (int) pct * 100 +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0001" && descMap.attrId == "0020") { + log.debug "Battery" + resultMap.name = "battery" + resultMap.value = getBatteryPercentage(Integer.parseInt(descMap.value, 16)) + } + else if (descMap.cluster == "0406" && descMap.attrId == "0000") { + log.debug "motion" + resultMap.name = "motion" + resultMap.value = descMap.value.endsWith("01") ? "active" : "inactive" + } + + return resultMap +} + + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0030': // Closed/No Motion/Dry + log.debug 'no motion' + resultMap.name = 'motion' + resultMap.value = 'inactive' + break + + case '0x0032': // Open/Motion/Wet + log.debug 'motion' + resultMap.name = 'motion' + resultMap.value = 'active' + break + + case '0x0032': // Tamper Alarm + log.debug 'motion with tamper alarm' + resultMap.name = 'motion' + resultMap.value = 'active' + break + + case '0x0033': // Battery Alarm + break + + case '0x0034': // Supervision Report + log.debug 'no motion with tamper alarm' + resultMap.name = 'motion' + resultMap.value = 'inactive' + break + + case '0x0035': // Restore Report + break + + case '0x0036': // Trouble/Failure + log.debug 'motion with failure alarm' + resultMap.name = 'motion' + resultMap.value = 'active' + break + + case '0x0038': // Test Mode + break + } + return resultMap +} + +def refresh() +{ + log.debug "refresh called" + [ + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 1 0x20 0x20 0x3600 0x3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1500", + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + ] + return configCmds + refresh() + enrollResponse() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy b/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy new file mode 100644 index 00000000000..2e3822bdef7 --- /dev/null +++ b/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy @@ -0,0 +1,346 @@ +/** + * NYCE Open/Close Sensor + * + * Copyright 2015 NYCE Sensors Inc. + * + * 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: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") { + capability "Battery" + capability "Configuration" + capability "Contact Sensor" + capability "Refresh" + + command "enrollResponse" + + + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011" + fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011" + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014" + fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014" + } + + simulator { + + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["contact"]) + details(["contact","battery","refresh"]) + } +} + +def parse(String description) { + Map map = [:] + + List listMap = [] + List listResult = [] + + log.debug "parse: Parse message: ${description}" + + if (description?.startsWith("enroll request")) { + List cmds = enrollResponse() + + log.debug "parse: enrollResponse() ${cmds}" + listResult = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + else { + if (description?.startsWith("zone status")) { + listMap = parseIasMessage(description) + } + else if (description?.startsWith("read attr -")) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith("catchall:")) { + map = parseCatchAllMessage(description) + } + // this condition is temperary to accomodate some unknown issue caused by SmartThings + // once they fix the bug, this condition is not needed + // The issue is most of the time when a device is removed thru the app, it takes couple + // times to pair again successfully + else if (description?.startsWith("updated")) { + List cmds = configure() + listResult = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + + // Create events from map or list of maps, whichever was returned + if (listMap) { + for (msg in listMap) { + listResult << createEvent(msg) + } + } + else if (map) { + listResult << createEvent(map) + } + } + + log.debug "parse: listResult ${listResult}" + return listResult +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + + if (shouldProcessMessage(cluster)) { + def msgStatus = cluster.data[2] + + log.debug "parseCatchAllMessage: msgStatus: ${msgStatus}" + if (msgStatus == 0) { + switch(cluster.clusterId) { + case 0x0001: + log.debug 'Battery' + resultMap.name = 'battery' + log.info "in parse catch all" + log.debug "battery value: ${cluster.data.last()}" + resultMap.value = getBatteryPercentage(cluster.data.last()) + break + case 0x0402: // temperature cluster + if (cluster.command == 0x01) { + if(cluster.data[3] == 0x29) { + def tempC = Integer.parseInt(cluster.data[-2..-1].reverse().collect{cluster.hex1(it)}.join(), 16) / 100 + resultMap = getTemperatureResult(getConvertedTemperature(tempC)) + log.debug "parseCatchAllMessage: Temp resultMap: ${resultMap}" + } + else { + log.debug "parseCatchAllMessage: Temperature cluster Wrong data type" + } + } + else { + log.debug "parseCatchAllMessage: Unhandled Temperature cluster command ${cluster.command}" + } + break + case 0x0405: // humidity cluster + if (cluster.command == 0x01) { + if(cluster.data[3] == 0x21) { + def hum = Integer.parseInt(cluster.data[-2..-1].reverse().collect{cluster.hex1(it)}.join(), 16) / 100 + resultMap = getHumidityResult(hum) + log.debug "parseCatchAllMessage: Hum resultMap: ${resultMap}" + } + else { + log.debug "parseCatchAllMessage: Humidity cluster wrong data type" + } + } + else { + log.debug "parseCatchAllMessage: Unhandled Humidity cluster command ${cluster.command}" + } + break + default: + break + } + } + else { + log.debug "parseCatchAllMessage: Message error code: Error code: ${msgStatus} ClusterID: ${cluster.clusterId} Command: ${cluster.command}" + } + } + + return resultMap +} + +private int getBatteryPercentage(int value) { + def minVolts = 2.3 + def maxVolts = 3.1 + def volts = value / 10 + def pct = (volts - minVolts) / (maxVolts - minVolts) + + //for battery that may have a higher voltage than 3.1V + if( pct > 1 ) + { + pct = 1 + } + + //the device actual shut off voltage is 2.25. When it drops to 2.3, there + //is actually still 0.05V, which is about 6% of juice left. + //setting the percentage to 6% so a battery low warning is issued + if( pct <= 0 ) + { + pct = 0.06 + } + return (int) pct * 100 +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { + map, param -> def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + Map resultMap = [:] + + log.debug "parseReportAttributeMessage: descMap ${descMap}" + + switch(descMap.cluster) { + case "0001": + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = getBatteryPercentage(convertHexToInt(descMap.value)) + break + default: + log.info descMap.cluster + log.info "cluster1" + break + } + + return resultMap +} + +private List parseIasMessage(String description) { + List parsedMsg = description.split(" ") + String msgCode = parsedMsg[2] + + List resultListMap = [] + Map resultMap_battery = [:] + Map resultMap_battery_state = [:] + Map resultMap_sensor = [:] + + // Relevant bit field definitions from ZigBee spec + def BATTERY_BIT = ( 1 << 3 ) + def TROUBLE_BIT = ( 1 << 6 ) + def SENSOR_BIT = ( 1 << 0 ) // it's ALARM1 bit from the ZCL spec + + // Convert hex string to integer + def zoneStatus = Integer.parseInt(msgCode[-4..-1],16) + + log.debug "parseIasMessage: zoneStatus: ${zoneStatus}" + + // Check each relevant bit, create map for it, and add to list + log.debug "parseIasMessage: Battery Status ${zoneStatus & BATTERY_BIT}" + log.debug "parseIasMessage: Trouble Status ${zoneStatus & TROUBLE_BIT}" + log.debug "parseIasMessage: Sensor Status ${zoneStatus & SENSOR_BIT}" + + /* Comment out this path to check the battery state to avoid overwriting the + battery value (Change log #2), but keep these conditions for later use + resultMap_battery_state.name = "battery_state" + if (zoneStatus & TROUBLE_BIT) { + resultMap_battery_state.value = "failed" + + resultMap_battery.name = "battery" + resultMap_battery.value = 0 + } + else { + if (zoneStatus & BATTERY_BIT) { + resultMap_battery_state.value = "low" + + // to generate low battery notification by the platform + resultMap_battery.name = "battery" + resultMap_battery.value = 15 + } + else { + resultMap_battery_state.value = "ok" + + // to clear the low battery state stored in the platform + // otherwise, there is no notification sent again + resultMap_battery.name = "battery" + resultMap_battery.value = 80 + } + } + */ + + resultMap_sensor.name = "contact" + resultMap_sensor.value = (zoneStatus & SENSOR_BIT) ? "open" : "closed" + + resultListMap << resultMap_battery_state + resultListMap << resultMap_battery + resultListMap << resultMap_sensor + + return resultListMap +} + +def configure() { + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + + def configCmds = [ + //battery reporting and heartbeat + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500", + + + // Writes CIE attribute on end device to direct reports to the hub's EUID + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + ] + + log.debug "configure: Write IAS CIE" + return configCmds +} + +def enrollResponse() { + [ + // Enrolling device into the IAS Zone + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + ] +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + + return array +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +def refresh() { + log.debug "Refreshing Battery" + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200" + ] +} diff --git a/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy b/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy new file mode 100644 index 00000000000..59b82bf50eb --- /dev/null +++ b/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy @@ -0,0 +1,51 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * On/Off Button Tile + * + * Author: SmartThings + * + * Date: 2013-05-01 + */ +metadata { + definition (name: "On/Off Button Tile", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Sensor" + } + + // simulator metadata + simulator { + } + + // UI tile definitions + tiles { + standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on" + state "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "off" + } + main "button" + details "button" + } +} + +def parse(String description) { +} + +def on() { + sendEvent(name: "switch", value: "on") +} + +def off() { + sendEvent(name: "switch", value: "off") +} + diff --git a/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy b/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy new file mode 100644 index 00000000000..5286a25301e --- /dev/null +++ b/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy @@ -0,0 +1,59 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "On/Off Shield", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Sensor" + } + + // Simulator metadata + simulator { + status "on": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6E" + status "off": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6666" + + // reply messages + reply "raw 0x0 { 00 00 0a 0a 6f 6e }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6E" + reply "raw 0x0 { 00 00 0a 0a 6f 66 66 }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6666" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + + main "switch" + details "switch" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def value = zigbee.parse(description)?.text + def name = value in ["on","off"] ? "switch" : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +// Commands sent to the device +def on() { + zigbee.smartShield(text: "on").format() +} + +def off() { + zigbee.smartShield(text: "off").format() +} diff --git a/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy b/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy new file mode 100644 index 00000000000..d05a5653f56 --- /dev/null +++ b/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy @@ -0,0 +1,53 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Contact Sensor" + capability "Sensor" + + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500", outClusters: "0000" + } + + // simulator metadata + simulator { + // status messages + status "open": "zone report :: type: 19 value: 0031" + status "closed": "zone report :: type: 19 value: 0030" + } + + // UI tile definitions + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + } + + main "contact" + details "contact" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def name = null + def value = description + if (zigbee.isZoneType19(description)) { + name = "contact" + value = zigbee.translateStatusZoneType19(description) ? "open" : "closed" + } + + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} diff --git a/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy new file mode 100644 index 00000000000..8b9d79bd20c --- /dev/null +++ b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy @@ -0,0 +1,386 @@ +/* + Osram Lightify Gardenspot Mini RGB + + Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling + that issue by using state variables +*/ + +metadata { + definition (name: "OSRAM LIGHTIFY Gardenspot mini RGB", namespace: "smartthings", author: "SmartThings") { + + capability "Color Temperature" + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Color Control" + + attribute "colorName", "string" + + command "setAdjustedColor" + + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB" + + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setAdjustedColor" + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { + state "colorName", label: '${currentValue}' + } + + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + main(["switch"]) + details(["switch", "refresh", "colorName", "levelSliderControl", "level", "rgbSelector"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + //log.info "description is $description" + if (description?.startsWith("catchall:")) { + if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) + { + def result = createEvent(name: "switch", value: "on") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) + { + if(!(description?.startsWith("catchall: 0104 0300"))){ + def result = createEvent(name: "switch", value: "off") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + } + } + else if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + log.trace "descMap : $descMap" + + if (descMap.cluster == "0300") { + if(descMap.attrId == "0000"){ //Hue Attribute + def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360) + log.debug "Hue value returned is $hueValue" + sendEvent(name: "hue", value: hueValue, displayed:false) + } + else if(descMap.attrId == "0001"){ //Saturation Attribute + def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) + log.debug "Saturation from refresh is $saturationValue" + sendEvent(name: "saturation", value: saturationValue, displayed:false) + } + } + else if(descMap.cluster == "0008"){ + def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) + log.debug "dimmer value is $dimmerValue" + sendEvent(name: "level", value: dimmerValue) + } + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } + + +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + setLevel(state?.levelValue) +} + +def zigbeeOff() { + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + zigbeeOff() +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1" + ] + +} + +def configure() { + state.levelValue = 100 + log.debug "Configuring Reporting and Bindings." + def configCmds = [ + + //Switch Reporting + "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", + + //Level Control Reporting + "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def poll(){ + log.debug "Poll is calling refresh" + refresh() +} + +def zigbeeSetLevel(level) { + "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" +} + +def setLevel(value) { + state.levelValue = (value==null) ? 100 : value + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << zigbeeOff() + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: state.levelValue) + def level = hex(state.levelValue * 255 / 100) + cmds << zigbeeSetLevel(level) + + //log.debug cmds + cmds +} + +//input Hue Integer values; returns color name for saturation 100% +private getColorName(hueValue){ + if(hueValue>360 || hueValue<0) + return + + hueValue = Math.round(hueValue / 100 * 360) + + log.debug "hue value is $hueValue" + + def colorName = "Color Mode" + if(hueValue>=0 && hueValue <= 4){ + colorName = "Red" + } + else if (hueValue>=5 && hueValue <=21 ){ + colorName = "Brick Red" + } + else if (hueValue>=22 && hueValue <=30 ){ + colorName = "Safety Orange" + } + else if (hueValue>=31 && hueValue <=40 ){ + colorName = "Dark Orange" + } + else if (hueValue>=41 && hueValue <=49 ){ + colorName = "Amber" + } + else if (hueValue>=50 && hueValue <=56 ){ + colorName = "Gold" + } + else if (hueValue>=57 && hueValue <=65 ){ + colorName = "Yellow" + } + else if (hueValue>=66 && hueValue <=83 ){ + colorName = "Electric Lime" + } + else if (hueValue>=84 && hueValue <=93 ){ + colorName = "Lawn Green" + } + else if (hueValue>=94 && hueValue <=112 ){ + colorName = "Bright Green" + } + else if (hueValue>=113 && hueValue <=135 ){ + colorName = "Lime" + } + else if (hueValue>=136 && hueValue <=166 ){ + colorName = "Spring Green" + } + else if (hueValue>=167 && hueValue <=171 ){ + colorName = "Turquoise" + } + else if (hueValue>=172 && hueValue <=187 ){ + colorName = "Aqua" + } + else if (hueValue>=188 && hueValue <=203 ){ + colorName = "Sky Blue" + } + else if (hueValue>=204 && hueValue <=217 ){ + colorName = "Dodger Blue" + } + else if (hueValue>=218 && hueValue <=223 ){ + colorName = "Navy Blue" + } + else if (hueValue>=224 && hueValue <=251 ){ + colorName = "Blue" + } + else if (hueValue>=252 && hueValue <=256 ){ + colorName = "Han Purple" + } + else if (hueValue>=257 && hueValue <=274 ){ + colorName = "Electric Indigo" + } + else if (hueValue>=275 && hueValue <=289 ){ + colorName = "Electric Purple" + } + else if (hueValue>=290 && hueValue <=300 ){ + colorName = "Orchid Purple" + } + else if (hueValue>=301 && hueValue <=315 ){ + colorName = "Magenta" + } + else if (hueValue>=316 && hueValue <=326 ){ + colorName = "Hot Pink" + } + else if (hueValue>=327 && hueValue <=335 ){ + colorName = "Deep Pink" + } + else if (hueValue>=336 && hueValue <=339 ){ + colorName = "Raspberry" + } + else if (hueValue>=340 && hueValue <=352 ){ + colorName = "Crimson" + } + else if (hueValue>=353 && hueValue <=360 ){ + colorName = "Red" + } + + colorName +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private evenHex(value){ + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() % 2 != 0) { + s = "0" + s + } + s +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +//Need to reverse array of size 2 +private byte[] reverseArray(byte[] array) { + byte tmp; + tmp = array[1]; + array[1] = array[0]; + array[0] = tmp; + return array +} + +def setAdjustedColor(value) { + log.debug "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.level = null // needed because color picker always sends 100 + setColor(adjusted) +} + +def setColor(value){ + log.trace "setColor($value)" + def max = 0xfe + + if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} + + def colorName = getColorName(value.hue) + sendEvent(name: "colorName", value: colorName) + + log.debug "color name is : $colorName" + sendEvent(name: "hue", value: value.hue, displayed:false) + sendEvent(name: "saturation", value: value.saturation, displayed:false) + def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) + def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) + + def cmd = [] + if (value.switch != "off" && device.latestValue("switch") == "off") { + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + cmd << "delay 150" + } + + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}" + cmd << "delay 150" + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}" + + if (value.level) { + state.levelValue = value.level + sendEvent(name: "level", value: value.level) + def level = hex(value.level * 255 / 100) + cmd << zigbeeSetLevel(level) + } + + if (value.switch == "off") { + cmd << "delay 150" + cmd << off() + } + + cmd +} diff --git a/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy b/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy new file mode 100644 index 00000000000..f8cf3238a95 --- /dev/null +++ b/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy @@ -0,0 +1,458 @@ +/* + Osram Flex RGBW Light Strip + + Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling + that issue by using state variables +*/ + +metadata { + definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") { + + capability "Color Temperature" + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Color Control" + + attribute "colorName", "string" + + command "setAdjustedColor" + + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW" + + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { + state "colorTemperature", label: '${currentValue} K' + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { + state "colorName", label: '${currentValue}' + } + + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setAdjustedColor" + } + + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + + main(["switch"]) + details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp", "rgbSelector"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + //log.info "description is $description" + if (description?.startsWith("catchall:")) { + if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) + { + def result = createEvent(name: "switch", value: "on") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) + { + if(!(description?.startsWith("catchall: 0104 0300"))){ + def result = createEvent(name: "switch", value: "off") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + } + + } + else if (description?.startsWith("read attr -")) { //for values returned after hitting refresh + def descMap = parseDescriptionAsMap(description) + log.trace "descMap : $descMap" + + if (descMap.cluster == "0300") { + if(descMap.attrId == "0007"){ + log.debug "in read attr" + log.debug descMap.value + def tempInMired = convertHexToInt(descMap.value) + def tempInKelvin = Math.round(1000000/tempInMired) + log.trace "temp in kelvin: $tempInKelvin" + sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false) + } + else if(descMap.attrId == "0008"){ //Color mode attribute + if(descMap.value == "00"){ + state.colorType = "rgb" + }else if(descMap.value == "02"){ + state.colorType = "white" + } + } + else if(descMap.attrId == "0000"){ //Hue Attribute + def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360) + log.debug "Hue value returned is $hueValue" + sendEvent(name: "hue", value: hueValue, displayed:false) + } + else if(descMap.attrId == "0001"){ //Saturation Attribute + def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) + log.debug "Saturation from refresh is $saturationValue" + sendEvent(name: "saturation", value: saturationValue, displayed:false) + } + } + else if(descMap.cluster == "0008"){ + def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) + log.debug "dimmer value is $dimmerValue" + sendEvent(name: "level", value: dimmerValue) + } + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "description is $description" + return result + } + + +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + setLevel(state?.levelValue) +} + +def zigbeeOff() { + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + zigbeeOff() +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 8" + ] + +} + +def configure() { + state.levelValue = 100 + state.colorType = "white" + log.debug "Configuring Reporting and Bindings." + def configCmds = [ + + //Switch Reporting + "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", + + //Level Control Reporting + "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def setColorTemperature(value) { + state?.colorType = "white" + if(value<101){ + value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 + } + + def tempInMired = Math.round(1000000/value) + def finalHex = swapEndianHex(hex(tempInMired, 4)) + def genericName = getGenericName(value) + log.debug "generic name is : $genericName" + + def cmds = [] + sendEvent(name: "colorTemperature", value: value, displayed:false) + sendEvent(name: "colorName", value: genericName) + + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" + + cmds +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def poll(){ + log.debug "Poll is calling refresh" + refresh() +} + +def zigbeeSetLevel(level) { + "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" +} + +def setLevel(value) { + state.levelValue = (value==null) ? 100 : value + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << zigbeeOff() + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: state.levelValue) + def level = hex(state.levelValue * 255 / 100) + cmds << zigbeeSetLevel(level) + + //log.debug cmds + cmds +} + +//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature +private getGenericName(value){ + def genericName = "White" + if(state?.colorType == "rgb"){ + genericName = "Color Mode" + } + else{ + if(value < 3300){ + genericName = "Soft White" + } else if(value < 4150){ + genericName = "Moonlight" + } else if(value < 5000){ + genericName = "Cool White" + } else if(value <= 6500){ + genericName = "Daylight" + } + } + + genericName +} + +//input Hue Integer values; returns color name for saturation 100% +private getColorName(hueValue){ + if(hueValue>360 || hueValue<0) + return + + hueValue = Math.round(hueValue / 100 * 360) + + log.debug "hue value is $hueValue" + + def colorName = "Color Mode" + if(hueValue>=0 && hueValue <= 4){ + colorName = "Red" + } + else if (hueValue>=5 && hueValue <=21 ){ + colorName = "Brick Red" + } + else if (hueValue>=22 && hueValue <=30 ){ + colorName = "Safety Orange" + } + else if (hueValue>=31 && hueValue <=40 ){ + colorName = "Dark Orange" + } + else if (hueValue>=41 && hueValue <=49 ){ + colorName = "Amber" + } + else if (hueValue>=50 && hueValue <=56 ){ + colorName = "Gold" + } + else if (hueValue>=57 && hueValue <=65 ){ + colorName = "Yellow" + } + else if (hueValue>=66 && hueValue <=83 ){ + colorName = "Electric Lime" + } + else if (hueValue>=84 && hueValue <=93 ){ + colorName = "Lawn Green" + } + else if (hueValue>=94 && hueValue <=112 ){ + colorName = "Bright Green" + } + else if (hueValue>=113 && hueValue <=135 ){ + colorName = "Lime" + } + else if (hueValue>=136 && hueValue <=166 ){ + colorName = "Spring Green" + } + else if (hueValue>=167 && hueValue <=171 ){ + colorName = "Turquoise" + } + else if (hueValue>=172 && hueValue <=187 ){ + colorName = "Aqua" + } + else if (hueValue>=188 && hueValue <=203 ){ + colorName = "Sky Blue" + } + else if (hueValue>=204 && hueValue <=217 ){ + colorName = "Dodger Blue" + } + else if (hueValue>=218 && hueValue <=223 ){ + colorName = "Navy Blue" + } + else if (hueValue>=224 && hueValue <=251 ){ + colorName = "Blue" + } + else if (hueValue>=252 && hueValue <=256 ){ + colorName = "Han Purple" + } + else if (hueValue>=257 && hueValue <=274 ){ + colorName = "Electric Indigo" + } + else if (hueValue>=275 && hueValue <=289 ){ + colorName = "Electric Purple" + } + else if (hueValue>=290 && hueValue <=300 ){ + colorName = "Orchid Purple" + } + else if (hueValue>=301 && hueValue <=315 ){ + colorName = "Magenta" + } + else if (hueValue>=316 && hueValue <=326 ){ + colorName = "Hot Pink" + } + else if (hueValue>=327 && hueValue <=335 ){ + colorName = "Deep Pink" + } + else if (hueValue>=336 && hueValue <=339 ){ + colorName = "Raspberry" + } + else if (hueValue>=340 && hueValue <=352 ){ + colorName = "Crimson" + } + else if (hueValue>=353 && hueValue <=360 ){ + colorName = "Red" + } + + colorName +} + + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private evenHex(value){ + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() % 2 != 0) { + s = "0" + s + } + s +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +//Need to reverse array of size 2 +private byte[] reverseArray(byte[] array) { + byte tmp; + tmp = array[1]; + array[1] = array[0]; + array[0] = tmp; + return array +} + +def setAdjustedColor(value) { + log.debug "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.level = null // needed because color picker always sends 100 + setColor(adjusted) +} + +def setColor(value){ + state?.colorType = "rgb" + log.trace "setColor($value)" + def max = 0xfe + + if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} + + def colorName = getColorName(value.hue) + log.debug "color name is : $colorName" + sendEvent(name: "colorName", value: colorName) + sendEvent(name: "colorTemperature", value: "--", displayed:false) + + + sendEvent(name: "hue", value: value.hue, displayed:false) + sendEvent(name: "saturation", value: value.saturation, displayed:false) + def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) + def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) + + def cmd = [] + if (value.switch != "off" && device.latestValue("switch") == "off") { + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + cmd << "delay 150" + } + + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}" + cmd << "delay 150" + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}" + + if (value.level) { + state.levelValue = value.level + sendEvent(name: "level", value: value.level) + def level = hex(value.level * 255 / 100) + cmd << zigbeeSetLevel(level) + } + + if (value.switch == "off") { + cmd << "delay 150" + cmd << off() + } + + cmd +} diff --git a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy new file mode 100644 index 00000000000..e09fb505b7b --- /dev/null +++ b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy @@ -0,0 +1,255 @@ +/* + Osram Tunable White 60 A19 bulb + + Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling + that issue by using state variables +*/ + +metadata { + definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") { + + capability "Color Temperature" + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Sensor" + + attribute "colorName", "string" + + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White" + + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { + state "colorTemperature", label: '${currentValue} K' + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { + state "colorName", label: '${currentValue}' + } + + + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + + main(["switch"]) + details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + //log.trace description + if (description?.startsWith("catchall:")) { + if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) + { + def result = createEvent(name: "switch", value: "on") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) + { + def result = createEvent(name: "switch", value: "off") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + + } + else if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + log.trace "descMap : $descMap" + + if (descMap.cluster == "0300") { + log.debug descMap.value + def tempInMired = convertHexToInt(descMap.value) + def tempInKelvin = Math.round(1000000/tempInMired) + log.trace "temp in kelvin: $tempInKelvin" + sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false) + } + else if(descMap.cluster == "0008"){ + def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) + log.debug "dimmer value is $dimmerValue" + sendEvent(name: "level", value: dimmerValue) + } + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } + + +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + setLevel(state?.levelValue) +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7" + ] + +} + +def configure() { + state.levelValue = 100 + log.debug "Confuguring Reporting and Bindings." + def configCmds = [ + + //Switch Reporting + "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", + + //Level Control Reporting + "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", + "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def setColorTemperature(value) { + if(value<101){ + value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 + } + + def tempInMired = Math.round(1000000/value) + def finalHex = swapEndianHex(hex(tempInMired, 4)) + def genericName = getGenericName(value) + log.debug "generic name is : $genericName" + + def cmds = [] + sendEvent(name: "colorTemperature", value: value, displayed:false) + sendEvent(name: "colorName", value: genericName) + + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" + + cmds +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def poll(){ + log.debug "Poll is calling refresh" + refresh() +} + +def setLevel(value) { + state.levelValue = (value==null) ? 100 : value + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: state.levelValue) + def level = hex(state.levelValue * 254 / 100) + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" + + //log.debug cmds + cmds +} + +//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature +private getGenericName(value){ + def genericName = "White" + if(value < 3300){ + genericName = "Soft White" + } else if(value < 4150){ + genericName = "Moonlight" + } else if(value < 5000){ + genericName = "Cool White" + } else if(value <= 6500){ + genericName = "Daylight" + } + + genericName +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +//Need to reverse array of size 2 +private byte[] reverseArray(byte[] array) { + byte tmp; + tmp = array[1]; + array[1] = array[0]; + array[0] = tmp; + return array +} diff --git a/devicetypes/smartthings/particulate-detector.src/particulate-detector.groovy b/devicetypes/smartthings/particulate-detector.src/particulate-detector.groovy new file mode 100644 index 00000000000..f13e32e54a2 --- /dev/null +++ b/devicetypes/smartthings/particulate-detector.src/particulate-detector.groovy @@ -0,0 +1,37 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Particulate Detector", namespace: "smartthings", author: "SmartThings") { + } + + // simulator metadata + simulator { + // TBD + } + + // tile definitions + tiles { + standardTile("particulate", "device.particulate", width: 2, height: 2) { + state "default", icon: "st.particulate.particulate.particulate", backgroundColor: "#ffffff" + } + + main "particulate" + details "particulate" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + // TBD +} diff --git a/devicetypes/smartthings/pet-feeder-shield.src/pet-feeder-shield.groovy b/devicetypes/smartthings/pet-feeder-shield.src/pet-feeder-shield.groovy new file mode 100644 index 00000000000..fa715856cad --- /dev/null +++ b/devicetypes/smartthings/pet-feeder-shield.src/pet-feeder-shield.groovy @@ -0,0 +1,54 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Pet Feeder Shield", namespace: "smartthings", author: "SmartThings") { + + command "feed" + } + + // Simulator metadata + simulator { + // status messages + status "ping": "catchall: 0104 0000 01 01 0040 00 6A67 00 00 0000 0A 00 0A70696E67" + status "response": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A4F4D4E4F4D4E4F4D4E4F4D" + + // reply messages + reply "raw 0x0 { 00 00 0a 0a 6f 6e }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6E" + reply "raw 0x0 { 00 00 0a 0a 6f 66 66 }": "catchall: 0104 0000 01 01 0040 00 0A21 00 00 0000 0A 00 0A6F6666" + } + + // UI tile definitions + tiles { + standardTile("feeder", "device.petFeederShield", width: 2, height: 2, canChangeBackground: true) { + state "default", action: "feed", icon: "st.shields.shields.pet-feeder", backgroundColor: "#ffffff" + } + + main "feeder" + details "feeder" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def value = zigbee.parse(description)?.text + def name = value && value != "ping" ? "response" : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +// Commands sent to the device +def feed() { + zigbee.smartShield(text: "feed" ).format() +} diff --git a/devicetypes/smartthings/plant-link.src/plant-link.groovy b/devicetypes/smartthings/plant-link.src/plant-link.groovy new file mode 100644 index 00000000000..275880bd1ed --- /dev/null +++ b/devicetypes/smartthings/plant-link.src/plant-link.groovy @@ -0,0 +1,103 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * PlantLink + * + * Author: SmartThings + * Date: 2013-12-17 + */ +metadata { + + definition (name: "Plant Link", namespace: "smartthings", author: "SmartThings") { + capability "Relative Humidity Measurement" + capability "Battery" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0405,FC08", outClusters: "0003" + } + + tiles { + valueTile("humidity", "device.humidity", width: 2, height: 2) { + state("humidity", label:'${currentValue}%', unit:"", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("battery", "device.battery") { + state "battery", label:'${currentValue}%', unit:"" + } + + main(["humidity", "battery"]) + details(["humidity", "battery"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "Parse description $description" + def map = [:] + if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + if (descMap.cluster == "0405" && descMap.attrId == "0000") { + log.debug "Humidity" + map.name = "humidity" + map.value = calculateHumidity(descMap.value) + } else if (descMap.cluster == "0001" && descMap.attrId == "0000") { + log.debug "Battery" + map.name = "battery" + map.value = calculateBattery(descMap.value) + } + } + + def result = null + if (map) { + result = createEvent(map) + } + log.debug "Parse returned $map" + return result +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +private calculateHumidity(value) { + //adc reading of 0x1ec0 produces a plant fuel level near 0 + //adc reading of 0x2100 produces a plant fuel level near 100% + def range = 576 //8448 - 7872 + def percent = (Integer.parseInt(value, 16) / range) * 100 + percent = Math.max(0.0, Math.min(percent, 100.0)) + percent +} + +private calculateBattery(value) { + def min = 2300 + def percent = (Integer.parseInt(value, 16) - min) / 10 + // Make sure our percentage is between 0 - 100 + percent = Math.max(0.0, Math.min(percent, 100.0)) + percent +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} diff --git a/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy b/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy new file mode 100644 index 00000000000..fd729a384a8 --- /dev/null +++ b/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy @@ -0,0 +1,261 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Z-Wave RGBW Light + * + * Author: SmartThings + * Date: 2015-7-12 + */ + +metadata { + definition (name: "RGBW Light", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + command "reset" + + fingerprint inClusters: "0x26,0x33" + fingerprint deviceId: "0x1102", inClusters: "0x26,0x33" + fingerprint inClusters: "0x33" + } + + simulator { + } + + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { + state "level", action:"switch level.setLevel" + } + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setColor" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false) { + state "colorTemperature", action:"setColorTemperature" + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } + + main(["switch"]) + details(["switch", "levelSliderControl", "rgbSelector", "reset", "colorTempControl", "refresh"]) +} + +def updated() { + response(refresh()) +} + +def parse(description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(physicalgraph.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + response(command(zwave.switchMultilevelV1.switchMultilevelGet())) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof physicalgraph.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 3500) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 3500) +} + +def setLevel(level) { + setLevel(level, 1) +} + +def setLevel(level, duration) { + if(level > 99) level = 99 + commands([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], (duration && duration < 12) ? (duration * 1000) : 3500) +} + +def refresh() { + commands([ + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 1000) +} + +def setSaturation(percent) { + log.debug "setSaturation($percent)" + setColor(saturation: percent) +} + +def setHue(value) { + log.debug "setHue($value)" + setColor(hue: value) +} + +def setColor(value) { + def result = [] + log.debug "setColor: ${value}" + if (value.hex) { + def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) + } else { + def hue = value.hue ?: device.currentValue("hue") + def saturation = value.saturation ?: device.currentValue("saturation") + if(hue == null) hue = 13 + if(saturation == null) saturation = 13 + def rgb = huesatToRGB(hue, saturation) + result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0) + } + + if(value.hue) sendEvent(name: "hue", value: value.hue) + if(value.hex) sendEvent(name: "color", value: value.hex) + if(value.switch) sendEvent(name: "switch", value: value.switch) + if(value.saturation) sendEvent(name: "saturation", value: value.saturation) + + commands(result) +} + +def setColorTemperature(percent) { + if(percent > 99) percent = 99 + int warmValue = percent * 255 / 99 + command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue))) +} + +def reset() { + log.debug "reset()" + sendEvent(name: "color", value: "#ffffff") + setColorTemperature(99) +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + float r = red / 255f + float g = green / 255f + float b = blue / 255f + float max = [r, g, b].max() + float delta = max - [r, g, b].min() + def hue = 13 + def saturation = 0 + if (max && delta) { + saturation = 100 * delta / max + if (r == max) { + hue = ((g - b) / delta) * 100 / 6 + } else if (g == max) { + hue = (2 + (b - r) / delta) * 100 / 6 + } else { + hue = (4 + (r - g) / delta) * 100 / 6 + } + } + [hue: hue, saturation: saturation, value: max * 100] +} + +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] + } +} diff --git a/devicetypes/smartthings/samsung-smart-tv.src/samsung-smart-tv.groovy b/devicetypes/smartthings/samsung-smart-tv.src/samsung-smart-tv.groovy new file mode 100644 index 00000000000..f53a6c31e1a --- /dev/null +++ b/devicetypes/smartthings/samsung-smart-tv.src/samsung-smart-tv.groovy @@ -0,0 +1,235 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Samsung TV + * + * Author: SmartThings (juano23@gmail.com) + * Date: 2015-01-08 + */ + +metadata { + definition (name: "Samsung Smart TV", namespace: "smartthings", author: "SmartThings") { + capability "switch" + + command "mute" + command "source" + command "menu" + command "tools" + command "HDMI" + command "Sleep" + command "Up" + command "Down" + command "Left" + command "Right" + command "chup" + command "chdown" + command "prech" + command "volup" + command "voldown" + command "Enter" + command "Return" + command "Exit" + command "Info" + command "Size" + } + + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "default", label:'TV', action:"switch.off", icon:"st.Electronics.electronics15", backgroundColor:"#ffffff" + } + standardTile("power", "device.switch", width: 1, height: 1, canChangeIcon: false) { + state "default", label:'', action:"switch.off", decoration: "flat", icon:"st.thermostat.heating-cooling-off", backgroundColor:"#ffffff" + } + standardTile("mute", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Mute', action:"mute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff" + } + standardTile("source", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Source', action:"source", icon:"st.Electronics.electronics15" + } + standardTile("tools", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Tools', action:"tools", icon:"st.secondary.tools" + } + standardTile("menu", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Menu', action:"menu", icon:"st.vents.vent" + } + standardTile("HDMI", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Source', action:"HDMI", icon:"st.Electronics.electronics15" + } + standardTile("Sleep", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Sleep', action:"Sleep", icon:"st.Bedroom.bedroom10" + } + standardTile("Up", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Up', action:"Up", icon:"st.thermostat.thermostat-up" + } + standardTile("Down", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Down', action:"Down", icon:"st.thermostat.thermostat-down" + } + standardTile("Left", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Left', action:"Left", icon:"st.thermostat.thermostat-left" + } + standardTile("Right", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Right', action:"Right", icon:"st.thermostat.thermostat-right" + } + standardTile("chup", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'CH Up', action:"chup", icon:"st.thermostat.thermostat-up" + } + standardTile("chdown", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'CH Down', action:"chdown", icon:"st.thermostat.thermostat-down" + } + standardTile("prech", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Pre CH', action:"prech", icon:"st.secondary.refresh-icon" + } + standardTile("volup", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Vol Up', action:"volup", icon:"st.thermostat.thermostat-up" + } + standardTile("voldown", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Vol Down', action:"voldown", icon:"st.thermostat.thermostat-down" + } + standardTile("Enter", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Enter', action:"Enter", icon:"st.illuminance.illuminance.dark" + } + standardTile("Return", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Return', action:"Return", icon:"st.secondary.refresh-icon" + } + standardTile("Exit", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Exit', action:"Exit", icon:"st.locks.lock.unlocked" + } + standardTile("Info", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Info', action:"Info", icon:"st.motion.acceleration.active" + } + standardTile("Size", "device.switch", decoration: "flat", canChangeIcon: false) { + state "default", label:'Picture Size', action:"Size", icon:"st.contact.contact.open" + } + main "switch" + details (["power","HDMI","Sleep","chup","prech","volup","chdown","mute","voldown", "menu", "Up", "tools", "Left", "Enter", "Right", "Return", "Down", "Exit", "Info","Size"]) +} + +def parse(String description) { + return null +} + +def off() { + log.debug "Turning TV OFF" + parent.tvAction("POWEROFF",device.deviceNetworkId) + sendEvent(name:"Command", value: "Power Off", displayed: true) +} + +def mute() { + log.trace "MUTE pressed" + parent.tvAction("MUTE",device.deviceNetworkId) + sendEvent(name:"Command", value: "Mute", displayed: true) +} + +def source() { + log.debug "SOURCE pressed" + parent.tvAction("SOURCE",device.deviceNetworkId) + sendEvent(name:"Command", value: "Source", displayed: true) +} + +def menu() { + log.debug "MENU pressed" + parent.tvAction("MENU",device.deviceNetworkId) +} + +def tools() { + log.debug "TOOLS pressed" + parent.tvAction("TOOLS",device.deviceNetworkId) + sendEvent(name:"Command", value: "Tools", displayed: true) +} + +def HDMI() { + log.debug "HDMI pressed" + parent.tvAction("HDMI",device.deviceNetworkId) + sendEvent(name:"Command sent", value: "Source", displayed: true) +} + +def Sleep() { + log.debug "SLEEP pressed" + parent.tvAction("SLEEP",device.deviceNetworkId) + sendEvent(name:"Command", value: "Sleep", displayed: true) +} + +def Up() { + log.debug "UP pressed" + parent.tvAction("UP",device.deviceNetworkId) +} + +def Down() { + log.debug "DOWN pressed" + parent.tvAction("DOWN",device.deviceNetworkId) +} + +def Left() { + log.debug "LEFT pressed" + parent.tvAction("LEFT",device.deviceNetworkId) +} + +def Right() { + log.debug "RIGHT pressed" + parent.tvAction("RIGHT",device.deviceNetworkId) +} + +def chup() { + log.debug "CHUP pressed" + parent.tvAction("CHUP",device.deviceNetworkId) + sendEvent(name:"Command", value: "Channel Up", displayed: true) +} + +def chdown() { + log.debug "CHDOWN pressed" + parent.tvAction("CHDOWN",device.deviceNetworkId) + sendEvent(name:"Command", value: "Channel Down", displayed: true) +} + +def prech() { + log.debug "PRECH pressed" + parent.tvAction("PRECH",device.deviceNetworkId) + sendEvent(name:"Command", value: "Prev Channel", displayed: true) +} + +def Exit() { + log.debug "EXIT pressed" + parent.tvAction("EXIT",device.deviceNetworkId) +} + +def volup() { + log.debug "VOLUP pressed" + parent.tvAction("VOLUP",device.deviceNetworkId) + sendEvent(name:"Command", value: "Volume Up", displayed: true) +} + +def voldown() { + log.debug "VOLDOWN pressed" + parent.tvAction("VOLDOWN",device.deviceNetworkId) + sendEvent(name:"Command", value: "Volume Down", displayed: true) +} + +def Enter() { + log.debug "ENTER pressed" + parent.tvAction("ENTER",device.deviceNetworkId) +} + +def Return() { + log.debug "RETURN pressed" + parent.tvAction("RETURN",device.deviceNetworkId) +} + +def Info() { + log.debug "INFO pressed" + parent.tvAction("INFO",device.deviceNetworkId) + sendEvent(name:"Command", value: "Info", displayed: true) +} + +def Size() { + log.debug "PICTURE_SIZE pressed" + parent.tvAction("PICTURE_SIZE",device.deviceNetworkId) + sendEvent(name:"Command", value: "Picture Size", displayed: true) +} \ No newline at end of file diff --git a/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy b/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy new file mode 100644 index 00000000000..82af5e4fdc0 --- /dev/null +++ b/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy @@ -0,0 +1,172 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Secure Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Sensor" + + fingerprint deviceId: "0x11", inClusters: "0x98" + } + + simulator { + status "on": "command: 9881, payload: 002603FF" + status "off": "command: 9881, payload: 00260300" + status "09%": "command: 9881, payload: 00260309" + status "10%": "command: 9881, payload: 0026030A" + status "33%": "command: 9881, payload: 00260321" + status "66%": "command: 9881, payload: 00260342" + status "99%": "command: 9881, payload: 00260363" + + // reply messages + reply "9881002001FF,delay 100,9881002602": "command: 9881, payload: 002603FF" + reply "988100200100,delay 100,9881002602": "command: 9881, payload: 00260300" + reply "988100200119,delay 100,9881002602": "command: 9881, payload: 00260319" + reply "988100200132,delay 100,9881002602": "command: 9881, payload: 00260332" + reply "98810020014B,delay 100,9881002602": "command: 9881, payload: 0026034B" + reply "988100200163,delay 100,9881002602": "command: 9881, payload: 00260363" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["switch", "levelSliderControl", "refresh"]) + } +} + +def parse(String description) { + if (description.startsWith("Err 106")) { + state.sec = 0 + createEvent(descriptionText: description, isStateChange: true) + } else { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x32:3, 0x98: 1]) + if (cmd) { + zwaveEvent(cmd) + } else { + log.debug("Couldn't zwave.parse '$description'") + null + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x26: 3, 0x32: 3]) + state.sec = 1 + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +private dimmerEvents(physicalgraph.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] + } else if (cmd.scale == 1) { + map = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] + } else if (cmd.scale == 2) { + map = [name: "power", value: cmd.scaledMeterValue, unit: "W"] + } else { + map = [name: "electric", value: cmd.scaledMeterValue, unit: ["pulses", "V", "A", "R/Z", ""][cmd.scale - 3]] + } + } else if (cmd.meterType == 2) { + map = [name: "gas", value: cmd.scaledMeterValue, unit: ["m^3", "ft^3", "", "pulses", ""][cmd.scale]] + } else if (cmd.meterType == 3) { + map = [name: "water", value: cmd.scaledMeterValue, unit: ["m^3", "ft^3", "gal"][cmd.scale]] + } + map.isStateChange = true // just show in activity + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +def on() { + secureSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet() + ], 3500) +} + +def off() { + secureSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet() + ], 3500) +} + +def setLevel(value) { + secureSequence([ + zwave.basicV1.basicSet(value: value), + zwave.switchMultilevelV1.switchMultilevelGet() + ]) +} + +def setLevel(value, duration) { + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + secure(zwave.switchMultilevelV2.switchMultilevelSet(value: value, dimmingDuration: dimmingDuration)) +} + +def refresh() { + secure(zwave.switchMultilevelV1.switchMultilevelGet()) +} + +private secure(physicalgraph.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private secureSequence(Collection commands, ...delayBetweenArgs) { + delayBetween(commands.collect{ secure(it) }, *delayBetweenArgs) +} diff --git a/devicetypes/smartthings/smart-body-analyzer.src/smart-body-analyzer.groovy b/devicetypes/smartthings/smart-body-analyzer.src/smart-body-analyzer.groovy new file mode 100644 index 00000000000..0072dcc0d68 --- /dev/null +++ b/devicetypes/smartthings/smart-body-analyzer.src/smart-body-analyzer.groovy @@ -0,0 +1,218 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Smart Body Analyzer + * + * Author: SmartThings + * Date: 2013-09-27 + */ +metadata { + definition (name: "Smart Body Analyzer", namespace: "smartthings", author: "SmartThings") { + capability "Polling" + + attribute "weight", "string" + attribute "leanMass", "string" + attribute "fatRatio", "string" + attribute "fatMass", "string" + attribute "pulse", "string" + + command "storeGraphImage" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { + state "default", label: "Withings", action: "", icon: "st.Bath.bath2", backgroundColor: "#FFFFFF" + } + + plotTile("weightGraph", "device.weight", "scatter", width:2, height:1, content:"weightPoints") + valueTile("weight", "device.weight", width: 1, height: 1) { + state("weight", label:'${currentValue} lbs', unit:"lbs", inactiveLabel: false) + } + + plotTile("fatGraph", "device.fatRatio", "scatter", width:2, height:1, content:"fatPoints") + valueTile("fatRatio", "device.fatRatio", width: 1, height: 1) { + state("fatRatio", label:'${currentValue}% bodyfat', unit:"bodyfat", inactiveLabel: false) + } + + plotTile("pulseGraph", "device.pulse", "scatter", width:2, height:1, content:"pulsePoints") + valueTile("pulse", "device.pulse", width: 1, height: 1, inactiveLabel: false) { + state("pulse", label:'${currentValue} bpm', unit:"bpm", + backgroundColors:[ + [value: 30, color: "#153591"], + [value: 60, color: "#1e9cbb"], + [value: 80, color: "#f1d801"], + [value: 100, color: "#d04e00"], + ] + ) + } + + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"polling.poll", icon:"st.secondary.refresh" + } + + main "icon" + details(["weightGraph", "weight", "fatGraph", "fatRatio", "pulseGraph", "pulse", "refresh"]) + } +} + +mappings { + path("/weightPoints") { + action: [ + GET: "weightPoints" + ] + } + path("/fatPoints") { + action: [ + GET: "fatPoints" + ] + } + path("/pulsePoints") { + action: [ + GET: "pulsePoints" + ] + } +} + +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def parse(Map event) { + log.debug "Parsing '${event}'" + + if(event.status == 200 && event.data) + { + parent.parse(event) + return null + } + else if(["weight", "leanMass", "fatRatio", "fatMass", "pulse"].contains(event.name) && event.date) + { + def dateString = event.date + data["measure.${event.name}.$dateString"] = event.value + + def old = "measure.${event.name}." + (new Date() - 30).format('yyyy-MM-dd') + data.findAll { it.key.startsWith("measure.${event.name}.") && it.key < old }.collect { it.key }.each { state.remove(it) } + } + + return event +} + +def storeGraphImage(String name, ByteArrayInputStream is, String contentType) { + storeImage(name, is, contentType) +} + +def poll() { + log.debug "Executing 'poll'" + parent.poll() +} + +def refresh() { + log.debug "Executing 'refresh'" + sendEvent(name:"refreshing", description:"Refreshing Withings data", displayed:true) + + return null +} + +def measurementPoints(name) { + def points = [] + + def wdata = normalizeMeasurementPoints(name) + log.debug "data: ${wdata}" + def allValues = wdata.collect { it.y } + log.debug "allValues: ${allValues}" + + def min = allValues.min() + def max = allValues.max() + + def minMax = [:] + minMax[min] = min + minMax[max] = max + log.debug "minMax: $minMax" + + wdata.reverse().each { it -> + points << plotPoint(it.x, it.y, minMax) + } + log.debug "points: ${points}" + + return points.reverse() +} + +private normalizeMeasurementPoints(name) { + def measurementData = data.findAll { it.key.startsWith("measure.${name}.") } + log.debug "measurementData: ${measurementData}" + + def normalizedData = [] + measurementData.each { k, v -> + def d = Date.parse('yyyy-MM-dd', k - "measure.${name}.") + Calendar cal = Calendar.getInstance(); + cal.setTime(d) + // BUG: DOES NOT HANDLE NEW YEAR PROPERLY + // Should concat YEAR + (PAD_LEFT(DAY_OF_YEAR)) + // 2013365 == Dec. 31, 2013 + // 2014001 == Jan. 1, 2014 + normalizedData << [x:cal.get(Calendar.DAY_OF_YEAR), y:v] + } + log.debug "normalizedData: ${normalizedData}" + + normalizedData.sort{ it.x } +} + +private plotPoint(x, y, minMax) { + def removed = minMax.remove(y) != null + + return [ + color:"", + fillColor: removed ? "" : "#f3f3f3", + symbolStyle:"elipse", + point:y, + label: removed ? "${y}" : "", + x:x, + y:y + ] +} + +def weightPoints() { + return [ + "title":"My Weight", + "plots":[ + "weight":[ + "points":measurementPoints("weight") + ] + ] + ] +} + +def pulsePoints() { + return [ + "title":"My Pulse", + "plots":[ + "pulse":[ + "points":measurementPoints("pulse") + ] + ] + ] +} + +def fatPoints() { + return [ + "title":"Bodyfat %", + "plots":[ + "fat":[ + "points":measurementPoints("fatRatio") + ] + ] + ] +} diff --git a/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy b/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy new file mode 100644 index 00000000000..6a7f511c804 --- /dev/null +++ b/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy @@ -0,0 +1,149 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartAlert Siren + * + * Author: SmartThings + * Date: 2013-03-05 + */ +metadata { + definition (name: "SmartAlert Siren", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Sensor" + capability "Alarm" + + command "test" + + fingerprint deviceId: "0x1100", inClusters: "0x26,0x71" + } + + simulator { + // reply messages + reply "2001FF,2002": "command: 2003, payload: FF" + reply "200100,2002": "command: 2003, payload: 00" + reply "200121,2002": "command: 2003, payload: 21" + reply "200142,2002": "command: 2003, payload: 42" + reply "2001FF,delay 3000,200100,2002": "command: 2003, payload: 00" + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label:'off', action:'alarm.strobe', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + state "strobe", label:'strobe!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + state "siren", label:'siren!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + standardTile("strobe", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "off", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc" + state "siren", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc" + state "strobe", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13" + state "both", label:'', action:'alarm.strobe', icon:"st.secondary.strobe", backgroundColor:"#e86d13" + } + standardTile("siren", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "off", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc" + state "strobe", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc" + state "siren", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13" + state "both", label:'', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#e86d13" + } + standardTile("test", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"test", icon:"st.secondary.test" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + } + main "alarm" + details(["alarm","strobe","siren","test","off"]) + } +} + +def on() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ] +} + +def off() { + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.basicV1.basicGet().format() + ] +} + +def test() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + "delay 3000", + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.basicV1.basicGet().format() + ] +} + +def strobe() { + [ + zwave.basicV1.basicSet(value: 0x21).format(), + zwave.basicV1.basicGet().format() + ] +} + +def siren() { + [ + zwave.basicV1.basicSet(value: 0x42).format(), + zwave.basicV1.basicGet().format() + ] +} + +def both() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ] +} + + +def parse(String description) { + log.debug "parse($description)" + def result = null + def cmd = zwave.parse(description, [0x20: 1]) + if (cmd) { + result = createEvents(cmd) + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + def switchValue = cmd.value ? "on" : "off" + def alarmValue + if (cmd.value == 0) { + alarmValue = "off" + } + else if (cmd.value <= 33) { + alarmValue = "strobe" + } + else if (cmd.value <= 66) { + alarmValue = "siren" + } + else { + alarmValue = "both" + } + [ + createEvent([name: "switch", value: switchValue, type: "digital", displayed: false]), + createEvent([name: "alarm", value: alarmValue, type: "digital"]) + ] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "UNEXPECTED COMMAND: $cmd" +} diff --git a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy new file mode 100644 index 00000000000..379d5bdbe5f --- /dev/null +++ b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy @@ -0,0 +1,351 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartPower Dimming Outlet (CentraLite) + * + * Author: SmartThings + * Date: 2013-12-04 + */ +metadata { + definition (name: "SmartPower Dimming Outlet", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Switch Level" + capability "Power Meter" + capability "Configuration" + capability "Refresh" + capability "Actuator" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-ZHAC" + + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + valueTile("power", "device.power", decoration: "flat") { + state "power", label:'${currentValue} W' + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "level", "power","refresh","levelSliderControl"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def finalResult = isKnownDescription(description) + if (finalResult != "false") { + log.info finalResult + if (finalResult.type == "update") { + log.info "$device updates: ${finalResult.value}" + } + else if (finalResult.type == "power") { + def powerValue = (finalResult.value as Integer)/10 + sendEvent(name: "power", value: powerValue) + + /* + Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 + + power level is an integer. The exact power level with correct units needs to be handled in the device type + to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 + */ + } + else { + sendEvent(name: finalResult.type, value: finalResult.value) + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug parseDescriptionAsMap(description) + } +} + +// Commands to device +def zigbeeCommand(cluster, attribute){ + "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" +} + +def off() { + zigbeeCommand("6", "0") +} + +def on() { + zigbeeCommand("6", "1") +} + +def setLevel(value) { + value = value as Integer + if (value == 0) { + off() + } + else { + if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + sendEvent(name: "level", value: value) + setLevelWithRate(value, "0000") //value is between 0 to 100 + } +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0B04 0x050B", "delay 500" + ] + +} + +def configure() { + onOffConfig() + levelConfig() + powerConfig() + refresh() +} + + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +//Need to reverse array of size 2 +private byte[] reverseArray(byte[] array) { + byte tmp; + tmp = array[1]; + array[1] = array[0]; + array[0] = tmp; + return array +} + +def parseDescriptionAsMap(description) { + if (description?.startsWith("read attr -")) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] + } + } + else if (description?.startsWith("catchall: ")) { + def seg = (description - "catchall: ").split(" ") + def zigbeeMap = [:] + zigbeeMap += [raw: (description - "catchall: ")] + zigbeeMap += [profileId: seg[0]] + zigbeeMap += [clusterId: seg[1]] + zigbeeMap += [sourceEndpoint: seg[2]] + zigbeeMap += [destinationEndpoint: seg[3]] + zigbeeMap += [options: seg[4]] + zigbeeMap += [messageType: seg[5]] + zigbeeMap += [dni: seg[6]] + zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] + zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] + zigbeeMap += [manufacturerId: seg[9]] + zigbeeMap += [command: seg[10]] + zigbeeMap += [direction: seg[11]] + zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { + it.join('') + } : []] + + zigbeeMap + } +} + +def isKnownDescription(description) { + if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { + def descMap = parseDescriptionAsMap(description) + if (descMap.cluster == "0006" || descMap.clusterId == "0006") { + isDescriptionOnOff(descMap) + } + else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){ + isDescriptionLevel(descMap) + } + else if (descMap.cluster == "0B04" || descMap.clusterId == "0B04"){ + isDescriptionPower(descMap) + } + } + else if(description?.startsWith("on/off:")) { + def switchValue = description?.endsWith("1") ? "on" : "off" + return [type: "switch", value : switchValue] + } + else { + return "false" + } +} + +def isDescriptionOnOff(descMap) { + def switchValue = "undefined" + if (descMap.cluster == "0006") { //cluster info from read attr + value = descMap.value + if (value == "01"){ + switchValue = "on" + } + else if (value == "00"){ + switchValue = "off" + } + } + else if (descMap.clusterId == "0006") { + //cluster info from catch all + //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 + //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 + if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ + switchValue = "on" + } + else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ + switchValue = "off" + } + else if(descMap.command=="07"){ + return [type: "update", value : "switch (0006) capability configured successfully"] + } + } + + if (switchValue != "undefined"){ + return [type: "switch", value : switchValue] + } + else { + return "false" + } + +} + +//@return - false or "success" or level [0-100] +def isDescriptionLevel(descMap) { + def dimmerValue = -1 + if (descMap.cluster == "0008"){ + //TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message + def value = convertHexToInt(descMap.value) + dimmerValue = Math.round(value * 100 / 255) + if(dimmerValue==0 && value > 0) { + dimmerValue = 1 //handling for non-zero hex value less than 3 + } + } + else if(descMap.clusterId == "0008") { + if(descMap.command=="0B"){ + return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent. + } + else if(descMap.command=="07"){ + return [type: "update", value : "level (0008) capability configured successfully"] + } + } + + if (dimmerValue != -1){ + return [type: "level", value : dimmerValue] + + } + else { + return "false" + } +} + +def isDescriptionPower(descMap) { + def powerValue = "undefined" + if (descMap.cluster == "0B04") { + if (descMap.attrId == "050b") { + powerValue = convertHexToInt(descMap.value) + } + } + else if (descMap.clusterId == "0B04") { + if(descMap.command=="07"){ + return [type: "update", value : "power (0B04) capability configured successfully"] + } + } + + if (powerValue != "undefined"){ + return [type: "power", value : powerValue] + } + else { + return "false" + } +} + + +def onOffConfig() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 6 0 0x10 0 600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s) +//min level change is 01 +def levelConfig() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 8 0 0x20 5 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) +//min change in value is 05 +def powerConfig() { + [ + //Meter (Power) Reporting + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 0x0B04 0x050B 0x2A 1 600 {05}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" + ] +} + +def setLevelWithRate(level, rate) { + if(rate == null){ + rate = "0000" + } + level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex + "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}" +} + +String convertToHexString(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/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy b/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy new file mode 100644 index 00000000000..ffe86b96071 --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy @@ -0,0 +1,55 @@ +metadata { + // Automatically generated. Make future change here. + definition (name: "SmartPower Outlet V1", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + + main "switch" + details "switch" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + if (description?.startsWith("catchall: 0104 000A")) { + log.debug "Dropping catchall for SmartPower Outlet" + return [] + } else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } +} + +// Commands to device +def on() { + 'zcl on-off on' +} + +def off() { + 'zcl on-off off' +} diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy new file mode 100644 index 00000000000..ae080dfeb8e --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -0,0 +1,106 @@ +/** + * CentraLite Switch + * + * Author: SmartThings + * Date: 2013-12-02 + */ +metadata { + // Automatically generated. Make future change here. + definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Configuration" + capability "Refresh" + capability "Sensor" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + valueTile("power", "device.power", decoration: "flat") { + state "power", label:'${currentValue} W' + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","power","refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "Parse description $description" + def name = null + def value = null + if (description?.startsWith("read attr -")) { + def descMap = parseDescriptionAsMap(description) + log.debug "Read attr: $description" + if (descMap.cluster == "0006" && descMap.attrId == "0000") { + name = "switch" + value = descMap.value.endsWith("01") ? "on" : "off" + } else { + def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim() + name = "power" + // assume 16 bit signed for encoding and power divisor is 10 + value = Integer.parseInt(reportValue, 16) / 10 + } + } else if (description?.startsWith("on/off:")) { + log.debug "Switch command" + name = "switch" + value = description?.endsWith(" 1") ? "on" : "off" + } + + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +// Commands to device +def on() { + 'zcl on-off on' +} + +def off() { + 'zcl on-off off' +} + +def meter() { + "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" +} + +def refresh() { + "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" +} + +def configure() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}" + ] +} diff --git a/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy b/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy new file mode 100644 index 00000000000..32ddb9de439 --- /dev/null +++ b/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy @@ -0,0 +1,421 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartSense Garage Door Multi + * + * Author: SmartThings + * Date: 2013-03-09 + */ +metadata { + definition (name: "SmartSense Garage Door Multi", namespace: "smartthings", author: "SmartThings") { + capability "Three Axis" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Signal Strength" + capability "Temperature Measurement" + capability "Sensor" + capability "Battery" + + attribute "status", "string" + attribute "door", "string" + } + + simulator { + status "open": "zone report :: type: 19 value: 0031" + status "closed": "zone report :: type: 19 value: 0030" + + status "acceleration": "acceleration: 1, rssi: 0, lqi: 0" + status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0" + + for (int i = 20; i <= 100; i += 10) { + status "${i}F": "contactState: 0, accelerationState: 0, temp: $i F, battery: 100, rssi: 100, lqi: 255" + } + + // kinda hacky because it depends on how it is installed + status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255" + } + + tiles { + standardTile("status", "device.status", width: 2, height: 2) { + state("closed", label:'${name}', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening") + state("open", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing") + state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e") + state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e") + } + standardTile("contact", "device.contact") { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + standardTile("acceleration", "device.acceleration", decoration: "flat") { + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + valueTile("temperature", "device.temperature", decoration: "flat") { + state("temperature", label:'${currentValue}°') + } + valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) { + state("threeAxis", label:'${currentValue}', unit:"") + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + /* + valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { + state "lqi", label:'${currentValue}% signal', unit:"" + } + */ + + main(["status","contact", "acceleration"]) + details(["status","contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/]) + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } +} + +def parse(String description) { + log.debug "parse($description)" + def results = null + + if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + // Ignore this in favor of orientation-based state + // results = parseSingleMessage(description) + } + else { + results = parseMultiSensorMessage(description) + } + log.debug "Parse returned ${results?.descriptionText}" + return results + +} + +def updated() { + log.debug "UPDATED" + def threeAxis = device.currentState("threeAxis") + if (threeAxis) { + def xyz = threeAxis.xyzValue + def value = Math.round(xyz.z) > 925 ? "open" : "closed" + sendEvent(name: "contact", value: value) + } +} + +def actuate() { + log.debug "Sending button press event" + sendEvent(name: "buttonPress", value: "true", isStateChange: true) +} + +private List parseMultiSensorMessage(description) { + def results = [] + if (isAccelerationMessage(description)) { + results = parseAccelerationMessage(description) + } + else if (isContactMessage(description)) { + results = parseContactMessage(description) + } + else if (isRssiLqiMessage(description)) { + results = parseRssiLqiMessage(description) + } + else if (isOrientationMessage(description)) { + results = parseOrientationMessage(description) + } + + results +} + +private List parseAccelerationMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('acceleration:')) { + def event = getAccelerationResult(part, description) + results << event + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private List parseContactMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('accelerationState:')) { + results << getAccelerationResult(part, description) + } + else if (part.startsWith('temp:')) { + results << getTempResult(part, description) + } + else if (part.startsWith('battery:')) { + results << getBatteryResult(part, description) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private List parseOrientationMessage(String description) { + def results = [] + def xyzResults = [x: 0, y: 0, z: 0] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('x:')) { + def unsignedX = part.split(":")[1].trim().toInteger() + def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX + xyzResults.x = signedX + } + else if (part.startsWith('y:')) { + def unsignedY = part.split(":")[1].trim().toInteger() + def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY + xyzResults.y = signedY + } + else if (part.startsWith('z:')) { + def unsignedZ = part.split(":")[1].trim().toInteger() + def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ + xyzResults.z = signedZ + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + def xyz = getXyzResult(xyzResults, description) + results << xyz + + // Looks for Z-axis orientation as virtual contact state + def a = xyz.value.split(',').collect{it.toInteger()} + def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1])) + def absValueZ = Math.abs(a[2]) + log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ" + + + if (absValueZ > 825 && absValueXY < 175) { + results << createEvent(name: "contact", value: "open", unit: "") + results << createEvent(name: "status", value: "open", unit: "") + results << createEvent(name: "door", value: "open", unit: "") + log.debug "STATUS: open" + } + else if (absValueZ < 75 && absValueXY > 825) { + results << createEvent(name: "contact", value: "closed", unit: "") + results << createEvent(name: "status", value: "closed", unit: "") + results << createEvent(name: "door", value: "closed", unit: "") + log.debug "STATUS: closed" + } + + results +} + +private List parseRssiLqiMessage(String description) { + def results = [] + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('lastHopRssi:')) { + results << getRssiResult(part, description, true) + } + else if (part.startsWith('lastHopLqi:')) { + results << getLqiResult(part, description, true) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private getAccelerationResult(part, description) { + def name = "acceleration" + def value = part.endsWith("1") ? "active" : "inactive" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: value, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getTempResult(part, description) { + def name = "temperature" + def temperatureScale = getTemperatureScale() + def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value°$temperatureScale" + def isStateChange = isTemperatureStateChange(device, name, value.toString()) + + [ + name: name, + value: value, + unit: temperatureScale, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getXyzResult(results, description) { + def name = "threeAxis" + def value = "${results.x},${results.y},${results.z}" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getBatteryResult(part, description) { + def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null + def name = "battery" + def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getRssiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopRssi" : "rssi" + def valueString = part.split(":")[1].trim() + def value = (Integer.parseInt(valueString) - 128).toString() + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value dBm" + + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: "dBm", + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +/** + * Use LQI (Link Quality Indicator) as a measure of signal strength. The values + * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal + * strength. Return as a percentage of 255. + * + * Note: To make the signal strength indicator more accurate, we could combine + * LQI with RSSI. + */ +private getLqiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopLqi" : "lqi" + def valueString = part.split(":")[1].trim() + def percentageOf = 255 + def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString() + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was: ${value}${unit}" + + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +private Boolean isAccelerationMessage(String description) { + // "acceleration: 1, rssi: 91, lqi: 255" + description ==~ /acceleration:.*rssi:.*lqi:.*/ +} + +private Boolean isContactMessage(String description) { + // "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255" + description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/ +} + +private Boolean isRssiLqiMessage(String description) { + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/ +} + +private Boolean isOrientationMessage(String description) { + // "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255" + description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/ +} diff --git a/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy new file mode 100644 index 00000000000..70e674917f6 --- /dev/null +++ b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy @@ -0,0 +1,432 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartSense Garage Door Sensor Button + * + * Author: SmartThings + * Date: 2013-03-09 + */ +metadata { + definition (name: "SmartSense Garage Door Sensor Button", namespace: "smartthings", author: "SmartThings") { + capability "Three Axis" + capability "Garage Door Control" + capability "Contact Sensor" + capability "Actuator" + capability "Acceleration Sensor" + capability "Signal Strength" + capability "Temperature Measurement" + capability "Sensor" + capability "Battery" + + attribute "status", "string" + attribute "buttonPress", "string" + + command "actuate" + } + + simulator { + status "acceleration": "acceleration: 1, rssi: 0, lqi: 0" + status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0" + + for (int i = 20; i <= 100; i += 10) { + status "${i}F": "contactState: 0, accelerationState: 0, temp: $i F, battery: 100, rssi: 100, lqi: 255" + } + + // kinda hacky because it depends on how it is installed + status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255" + } + + tiles { + standardTile("status", "device.status", width: 2, height: 2) { + state("closed", label:'${name}', icon:"st.doors.garage.garage-closed", action: "actuate", backgroundColor:"#79b821", nextState:"opening") + state("open", label:'${name}', icon:"st.doors.garage.garage-open", action: "actuate", backgroundColor:"#ffa81e", nextState:"closing") + state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e") + state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e") + } + standardTile("contact", "device.contact") { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + standardTile("acceleration", "device.acceleration", decoration: "flat") { + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + valueTile("temperature", "device.temperature", decoration: "flat") { + state("temperature", label:'${currentValue}°') + } + valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) { + state("threeAxis", label:'${currentValue}', unit:"") + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + /* + valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { + state "lqi", label:'${currentValue}% signal', unit:"" + } + */ + + main(["status","contact", "acceleration"]) + details(["status","contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/]) + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } +} + +def open() { + if (device.currentValue("status") != "open") { + log.debug "Sending button press event to open door" + sendEvent(name: "buttonPress", value: "true", isStateChange: true, unit: "") + } + else { + log.debug "Not opening door since it is already open" + } +} + +def close() { + if (device.currentValue("status") != "closed") { + log.debug "Sending button press event to close door" + sendEvent(name: "buttonPress", value: "true", isStateChange: true, unit: "") + } + else { + log.debug "Not closing door since it is already closed" + } +} + +def parse(String description) { + log.debug "parse($description)" + def results = null + + if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + // Ignore this in favor of orientation-based state + // results = parseSingleMessage(description) + } + else { + results = parseMultiSensorMessage(description) + } + log.debug "Parse returned ${results?.descriptionText}" + return results + +} + +def actuate() { + log.debug "Sending button press event" + sendEvent(name: "buttonPress", value: "true", isStateChange: true, unit: "") +} + +private List parseMultiSensorMessage(description) { + def results = [] + if (isAccelerationMessage(description)) { + results = parseAccelerationMessage(description) + } + else if (isContactMessage(description)) { + results = parseContactMessage(description) + } + else if (isRssiLqiMessage(description)) { + results = parseRssiLqiMessage(description) + } + else if (isOrientationMessage(description)) { + results = parseOrientationMessage(description) + } + + results +} + +private List parseAccelerationMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('acceleration:')) { + def event = getAccelerationResult(part, description) + results << event + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private List parseContactMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('accelerationState:')) { + results << getAccelerationResult(part, description) + } + else if (part.startsWith('temp:')) { + results << getTempResult(part, description) + } + else if (part.startsWith('battery:')) { + results << getBatteryResult(part, description) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private List parseOrientationMessage(String description) { + def results = [] + def xyzResults = [x: 0, y: 0, z: 0] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('x:')) { + def unsignedX = part.split(":")[1].trim().toInteger() + def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX + xyzResults.x = signedX + } + else if (part.startsWith('y:')) { + def unsignedY = part.split(":")[1].trim().toInteger() + def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY + xyzResults.y = signedY + } + else if (part.startsWith('z:')) { + def unsignedZ = part.split(":")[1].trim().toInteger() + def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ + xyzResults.z = signedZ + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + def xyz = getXyzResult(xyzResults, description) + results << xyz + + // Looks for Z-axis orientation as virtual contact state + def a = xyz.value.split(',').collect{it.toInteger()} + def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1])) + def absValueZ = Math.abs(a[2]) + log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ" + + + if (absValueZ > 825 && absValueXY < 175) { + results << createEvent(name: "contact", value: "open", unit: "") + results << createEvent(name: "status", value: "open", unit: "") + results << createEvent(name: "door", value: "open", unit: "") + log.debug "STATUS: open" + } + else if (absValueZ < 75 && absValueXY > 825) { + results << createEvent(name: "contact", value: "closed", unit: "") + results << createEvent(name: "status", value: "closed", unit: "") + results << createEvent(name: "door", value: "closed", unit: "") + log.debug "STATUS: closed" + } + + results +} + +private List parseRssiLqiMessage(String description) { + def results = [] + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('lastHopRssi:')) { + results << getRssiResult(part, description, true) + } + else if (part.startsWith('lastHopLqi:')) { + results << getLqiResult(part, description, true) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private getAccelerationResult(part, description) { + def name = "acceleration" + def value = part.endsWith("1") ? "active" : "inactive" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: value, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getTempResult(part, description) { + def name = "temperature" + def temperatureScale = getTemperatureScale() + def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value°$temperatureScale" + def isStateChange = isTemperatureStateChange(device, name, value.toString()) + + [ + name: name, + value: value, + unit: temperatureScale, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getXyzResult(results, description) { + def name = "threeAxis" + def value = "${results.x},${results.y},${results.z}" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getBatteryResult(part, description) { + def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null + def name = "battery" + def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getRssiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopRssi" : "rssi" + def valueString = part.split(":")[1].trim() + def value = (Integer.parseInt(valueString) - 128).toString() + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value dBm" + + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: "dBm", + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +/** + * Use LQI (Link Quality Indicator) as a measure of signal strength. The values + * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal + * strength. Return as a percentage of 255. + * + * Note: To make the signal strength indicator more accurate, we could combine + * LQI with RSSI. + */ +private getLqiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopLqi" : "lqi" + def valueString = part.split(":")[1].trim() + def percentageOf = 255 + def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString() + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was: ${value}${unit}" + + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +private Boolean isAccelerationMessage(String description) { + // "acceleration: 1, rssi: 91, lqi: 255" + description ==~ /acceleration:.*rssi:.*lqi:.*/ +} + +private Boolean isContactMessage(String description) { + // "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255" + description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/ +} + +private Boolean isRssiLqiMessage(String description) { + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/ +} + +private Boolean isOrientationMessage(String description) { + // "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255" + description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/ +} diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy new file mode 100644 index 00000000000..849cf001e84 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -0,0 +1,325 @@ +/** + * SmartSense Moisture Sensor + * + * Copyright 2014 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: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") { + capability "Configuration" + capability "Battery" + capability "Refresh" + capability "Temperature Measurement" + capability "Water Sensor" + + command "enrollResponse" + + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu" + + } + + simulator { + + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery' + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "water", "temperature" + details(["water", "temperature", "battery", "refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0x0402: + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getMoistureResult('dry') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getMoistureResult('wet') + break + + case '0x0022': // Tamper Alarm + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + log.debug 'dry with tamper alarm' + resultMap = getMoistureResult('dry') + break + + case '0x0025': // Restore Report + log.debug 'water with tamper alarm' + resultMap = getMoistureResult('wet') + break + + case '0x0026': // Trouble/Failure + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def volts = rawValue / 10 + def descriptionText + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + + return result +} + +private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getMoistureResult(value) { + log.debug 'water' + String descriptionText = "${device.displayName} is ${value}" + return [ + name: 'water', + value: value, + descriptionText: descriptionText + ] +} + +def refresh() +{ + log.debug "Refreshing Temperature and Battery" + [ + + + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1000", + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1000", + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy new file mode 100644 index 00000000000..6c3298acfad --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy @@ -0,0 +1,137 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "SmartSense Moisture", namespace: "smartthings", author: "SmartThings") { + capability "Water Sensor" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86" + fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86" + } + + simulator { + status "dry": "command: 7105, payload: 00 00 00 FF 05 FE 00 00" + status "wet": "command: 7105, payload: 00 FF 00 FF 05 02 00 00" + status "overheated": "command: 7105, payload: 00 00 00 FF 04 02 00 00" + status "freezing": "command: 7105, payload: 00 00 00 FF 04 05 00 00" + status "normal": "command: 7105, payload: 00 00 00 FF 04 FE 00 00" + for (int i = 0; i <= 100; i += 20) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage() + } + } + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + standardTile("temperature", "device.temperature", width: 2, height: 2) { + state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" + state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0" + state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + main (["water", "temperature"]) + details(["water", "temperature", "battery"]) + } +} + +def parse(String description) { + def result = [] + def parsedZwEvent = zwave.parse(description, [0x30: 1, 0x71: 2, 0x84: 1]) + + if(parsedZwEvent) { + if(parsedZwEvent.CMD == "8407") { + def lastStatus = device.currentState("battery") + def ageInMinutes = lastStatus ? (new Date().time - lastStatus.date.time)/60000 : 600 + log.debug "Battery status was last checked ${ageInMinutes} minutes ago" + + if (ageInMinutes >= 600) { + log.debug "Battery status is outdated, requesting battery report" + result << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + } + result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + } + result << createEvent( zwaveEvent(parsedZwEvent) ) + } + if(!result) result = [ descriptionText: parsedZwEvent, displayed: false ] + log.debug "Parse returned ${result}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + [descriptionText: "${device.displayName} woke up", isStateChange: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + def map = [:] + map.name = "water" + map.value = cmd.sensorValue ? "wet" : "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + map +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + if(cmd.batteryLevel == 0xFF) { + map.name = "battery" + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.displayed = true + } else { + map.name = "battery" + map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 + map.unit = "%" + map.displayed = false + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) +{ + def map = [:] + if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_WATER) { + map.name = "water" + map.value = cmd.alarmLevel ? "wet" : "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + } + if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) { + map.name = "temperature" + if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"} + if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"} + if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"} + if(cmd.zwaveAlarmEvent == 4) { map.value = "changing temperature rapidly"} + if(cmd.zwaveAlarmEvent == 5) { map.value = "freezing"} + if(cmd.zwaveAlarmEvent == 6) { map.value = "freezing"} + if(cmd.zwaveAlarmEvent == 254) { map.value = "normal"} + map.descriptionText = "${device.displayName} is ${map.value}" + } + + map +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) +{ + log.debug "COMMAND CLASS: $cmd" +} + diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy new file mode 100644 index 00000000000..8d3cb05bf71 --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -0,0 +1,350 @@ +/** + * SmartSense Motion/Temp Sensor + * + * Copyright 2014 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: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Temperature Measurement" + capability "Refresh" + + command "enrollResponse" + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326" + } + + simulator { + status "active": "zone report :: type: 19 value: 0031" + status "inactive": "zone report :: type: 19 value: 0030" + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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") { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery' + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["motion","temperature"]) + details(["motion","temperature","battery","refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0x0402: + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + + case 0x0406: + log.debug 'motion' + resultMap.name = 'motion' + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private int getHumidity(value) { + return Math.round(Double.parseDouble(value)) +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + else if (descMap.cluster == "0406" && descMap.attrId == "0000") { + def value = descMap.value.endsWith("01") ? "active" : "inactive" + resultMap = getMotionResult(value) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getMotionResult('inactive') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getMotionResult('active') + break + + case '0x0022': // Tamper Alarm + log.debug 'motion with tamper alarm' + resultMap = getMotionResult('active') + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + log.debug 'no motion with tamper alarm' + resultMap = getMotionResult('inactive') + break + + case '0x0025': // Restore Report + break + + case '0x0026': // Trouble/Failure + log.debug 'motion with failure alarm' + resultMap = getMotionResult('active') + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + log.debug rawValue + + def result = [ + name: 'battery', + value: '--' + ] + + def volts = rawValue / 10 + def descriptionText + + if (rawValue == 0) {} + else { + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else if (volts > 0){ + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + }} + + return result +} + +private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getMotionResult(value) { + log.debug 'motion' + String linkText = getLinkText(device) + String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped" + return [ + name: 'motion', + value: value, + descriptionText: descriptionText + ] +} + +def refresh() +{ + log.debug "refresh called" + [ + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x001 {${device.zigbeeId}} {}", "delay 200", + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}" + + + ] + return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 ${endpointId}" + + ] +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy new file mode 100644 index 00000000000..4ec3d9a6582 --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy @@ -0,0 +1,340 @@ +/** + * SmartSense Motion/Temp Sensor + * + * Copyright 2014 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: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Temperature Measurement" + capability "Refresh" + + command "enrollResponse" + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326" + } + + simulator { + status "active": "zone report :: type: 19 value: 0031" + status "inactive": "zone report :: type: 19 value: 0030" + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + 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") { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery' + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["motion","temperature"]) + details(["motion","temperature","battery","refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0x0402: + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + + case 0x0406: + log.debug 'motion' + resultMap.name = 'motion' + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private int getHumidity(value) { + return Math.round(Double.parseDouble(value)) +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + else if (descMap.cluster == "0406" && descMap.attrId == "0000") { + def value = descMap.value.endsWith("01") ? "active" : "inactive" + resultMap = getMotionResult(value) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getMotionResult('inactive') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getMotionResult('active') + break + + case '0x0022': // Tamper Alarm + log.debug 'motion with tamper alarm' + resultMap = getMotionResult('active') + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + log.debug 'no motion with tamper alarm' + resultMap = getMotionResult('inactive') + break + + case '0x0025': // Restore Report + break + + case '0x0026': // Trouble/Failure + log.debug 'motion with failure alarm' + resultMap = getMotionResult('active') + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def volts = rawValue / 10 + def descriptionText + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + + return result +} + +private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getMotionResult(value) { + log.debug 'motion' + String linkText = getLinkText(device) + String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped" + return [ + name: 'motion', + value: value, + descriptionText: descriptionText + ] +} + +def refresh() +{ + log.debug "refresh called" + [ + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1500", + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy b/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy new file mode 100644 index 00000000000..491e8eb7606 --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy @@ -0,0 +1,293 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "SmartSense Motion", namespace: "smartthings", author: "SmartThings") { + capability "Signal Strength" + capability "Motion Sensor" + capability "Sensor" + capability "Battery" + + fingerprint profileId: "0104", deviceId: "013A", inClusters: "0000", outClusters: "0006" + fingerprint profileId: "FC01", deviceId: "013A" + } + + simulator { + status "active": "zone report :: type: 19 value: 0031" + status "inactive": "zone report :: type: 19 value: 0030" + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + /* + valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { + state "lqi", label:'${currentValue}% signal', unit:"" + } + */ + + main "motion" + details(["motion", "battery"/*, "lqi"*/]) + } +} + +def parse(String description) { + def results + if (isZoneType19(description) || !isSupportedDescription(description)) { + results = parseBasicMessage(description) + } + else if (isMotionStatusMessage(description)){ + results = parseMotionStatusMessage(description) + } + + results +} + +private Map parseBasicMessage(description) { + def name = parseName(description) + def value = parseValue(description) + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def handlerName = value + def isStateChange = isStateChange(device, name, value) + + def results = [ + name: name, + value: value, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] + log.debug "Parse returned $results.descriptionText" + return results +} + +private String parseName(String description) { + if (isSupportedDescription(description)) { + return "motion" + } + null +} + +private String parseValue(String description) { + if (isZoneType19(description)) { + if (translateStatusZoneType19(description)) { + return "active" + } + else { + return "inactive" + } + } + + description +} + +private parseDescriptionText(String linkText, String value, String description) { + switch(value) { + case "active": return "$linkText detected motion" + case "inactive": return "$linkText motion has stopped" + default: return value + } +} + +private Boolean isMotionStatusMessage(String description) { + // "raw:7D360000001D55FF, dni:7D36, motion:00, battery:00, powerSource:00, rssi:1D, lqi:55, other:FF" - old (incorrect dev-conn parse) + // "raw:7D360000001D55FF, dni:7D36, motion:00, powerSource:0000, battery:00, rssi:1D, lqi:55, other:FF" - old (correct dev-conn parse) + // "raw:7D360000001D55FF, dni:7D36, motion:00, powerSource:00, battery:00, batteryDivisor:00, rssi:1D, lqi:55, other:FF" - new + description ==~ /raw:.*dni:.*motion:.*battery:.*powerSource:.*rssi:.*lqi:.*/ || description ==~ /raw:.*dni:.*motion:.*powerSource:.*battery:.*rssi:.*lqi:.*/ +} + +private List parseMotionStatusMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('motion:')) { + def motionResult = getMotionResult(part, description) + if (motionResult) { + results << motionResult + } + } + else if (part.startsWith('powerSource:')) { + def powerSourceResult = getPowerSourceResult(part, description) + if (powerSourceResult) { + results << powerSourceResult + } + } + else if (part.startsWith('battery:')) { + def batteryResult = getBatteryResult(part, description) + if (batteryResult) { + results << batteryResult + } + } + else if (part.startsWith('rssi:')) { + def rssiResult = getRssiResult(part, description) + if (rssiResult) { + results << rssiResult + } + } + else if (part.startsWith('lqi:')) { + def lqiResult = getLqiResult(part, description) + if (lqiResult) { + results << lqiResult + } + } + } + + results +} + +private getMotionResult(part, description) { + def name = "motion" + def valueString = part.split(":")[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + def value = valueInt == 0 ? "inactive" : "active" + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + linkText: linkText, + descriptionText: descriptionText, + handlerName: value, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getPowerSourceResult(part, description) { + def name = "powerSource" + def valueString = part.split(":")[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + def value = valueInt == 0 ? "battery" : "powered" + def linkText = getLinkText(device) + def descriptionText + if (value == "battery") { + descriptionText = "$linkText is ${value} powered" + } + else { + descriptionText = "$linkText is plugged in" + } + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getBatteryResult(part, description) { + def name = "battery" + def valueString = part.split(":")[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + + // Temporarily disallow zero as a valid result b/c the current firmware has a bug where zero is only value being sent + // This effectively disables battery reporting for this device, so needs to be removed once FW is updated + if (valueInt == 0) return null + + + def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null + def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText battery was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getRssiResult(part, description) { + def name = "rssi" + def parts = part.split(":") + if (parts.size() != 2) return null + + def valueString = parts[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + def value = (valueInt - 128).toString() + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value dBm" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: "dBm", + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +/** + * Use LQI (Link Quality Indicator) as a measure of signal strength. The values + * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal + * strength. Return as a percentage of 255. + * + * Note: To make the signal strength indicator more accurate, we could combine + * LQI with RSSI. + */ +private getLqiResult(part, description) { + def name = "lqi" + def parts = part.split(":") + if (parts.size() != 2) return null + + def valueString = parts[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + def percentageOf = 255 + def value = Math.round((valueInt / percentageOf * 100)).toString() + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText Signal (LQI) was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy new file mode 100644 index 00000000000..f89bacdd97d --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -0,0 +1,485 @@ +/** + * SmartSense Multi + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + + metadata { + definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { + + capability "Three Axis" + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Refresh" + capability "Temperature Measurement" + + command "enrollResponse" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S" + + } + + simulator { + status "open": "zone report :: type: 19 value: 0031" + status "closed": "zone report :: type: 19 value: 0030" + + status "acceleration": "acceleration: 1" + status "no acceleration": "acceleration: 0" + + for (int i = 10; i <= 50; i += 10) { + status "temp ${i}C": "contactState: 0, accelerationState: 0, temp: $i C, battery: 100" + } + + // kinda hacky because it depends on how it is installed + status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0" + status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0" + status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0" + status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000" + } + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + standardTile("acceleration", "device.acceleration") { + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + } + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) { + state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + + main (["contact", "acceleration", "temperature"]) + details(["contact","acceleration", "temperature", "3axis", "battery", "refresh"]) + } + } + + def parse(String description) { + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result + } + + private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0xFC02: + log.debug 'ACCELERATION' + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap + } + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private int getHumidity(value) { + return Math.round(Double.parseDouble(value)) +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "FC02" && descMap.attrId == "0010") { + resultMap = getAccelerationResult(descMap.value) + } + else if (descMap.cluster == "FC02" && descMap.attrId == "0012") { + resultMap = parseAxis(descMap.value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getContactResult('closed') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getContactResult('open') + break + + case '0x0022': // Tamper Alarm + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + resultMap = getContactResult('closed') + break + + case '0x0025': // Restore Report + resultMap = getContactResult('open') + break + + case '0x0026': // Trouble/Failure + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } + } + + private Map getBatteryResult(rawValue) { + log.debug "Battery" + log.debug rawValue + def linkText = getLinkText(device) + + def result = [ + name: 'battery', + value: '--' + ] + + def volts = rawValue / 10 + def descriptionText + + if (rawValue == 255) {} + else { + + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + }} + + return result + } + + private Map getTemperatureResult(value) { + log.debug "Temperature" + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] + } + + private Map getContactResult(value) { + log.debug "Contact" + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + return [ + name: 'contact', + value: value, + descriptionText: descriptionText + ] + } + + private getAccelerationResult(numValue) { + log.debug "Acceleration" + def name = "acceleration" + def value = numValue.endsWith("1") ? "active" : "inactive" + //def linkText = getLinkText(device) + def descriptionText = "was $value" + def isStateChange = isStateChange(device, name, value) + [ + name: name, + value: value, + descriptionText: descriptionText, + isStateChange: isStateChange + ] + } + + def refresh() + { + log.debug "Refreshing Values " + [ + + /* sensitivity - default value (8) */ + + "zcl mfg-code 0x104E", "delay 200", + "zcl global write 0xFC02 0 0x20 {02}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 400", + + "zcl mfg-code 0x104E", "delay 200", + "zcl global read 0xFC02 0x0000", "delay 200", + "send 0x${device.deviceNetworkId} 1 1","delay 400", + + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200", + + "zcl mfg-code 0x104E", "delay 200", + "zcl global read 0xFC02 0x0010", "delay 100", + "send 0x${device.deviceNetworkId} 1 1","delay 400", + + "zcl mfg-code 0x104E", "delay 200", + "zcl global read 0xFC02 0x0012", "delay 100", + "send 0x${device.deviceNetworkId} 1 1","delay 400", + + "zcl mfg-code 0x104E", "delay 200", + "zcl global read 0xFC02 0x0013", "delay 100", + "send 0x${device.deviceNetworkId} 1 1","delay 400", + + "zcl mfg-code 0x104E", "delay 200", + "zcl global read 0xFC02 0x0014", "delay 100", + "send 0x${device.deviceNetworkId} 1 1" + + ] + } + + def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Configuring Reporting" + + def configCmds = [ + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200", + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200", + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + + "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200", + "zcl mfg-code 0x104E", + "zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + + "zcl mfg-code 0x104E", + "zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + + "zcl mfg-code 0x104E", + "zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + + "zcl mfg-code 0x104E", + "zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" + + ] + + return configCmds + refresh() +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} + + +private Map parseAxis(String description) { + log.debug "parseAxis" + def xyzResults = [x: 0, y: 0, z: 0] + def parts = description.split("2900") + parts[0] = "12" + parts[0] + parts.each { part -> + part = part.trim() + if (part.startsWith("12")) { + def unsignedX = hexToInt(part.split("12")[1].trim()) + def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX + xyzResults.x = signedX + log.debug "X Part: ${signedX}" + } + else if (part.startsWith("13")) { + def unsignedY = hexToInt(part.split("13")[1].trim()) + def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY + xyzResults.y = signedY + log.debug "Y Part: ${signedY}" + } + else if (part.startsWith("14")) { + def unsignedZ = hexToInt(part.split("14")[1].trim()) + def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ + xyzResults.z = signedZ + log.debug "Z Part: ${signedZ}" + } + } + + getXyzResult(xyzResults, description) +} + + + +private Map getXyzResult(results, description) { + def name = "threeAxis" + def value = "${results.x},${results.y},${results.z}" + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private hexToInt(value) { + new BigInteger(value, 16) +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} + diff --git a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy new file mode 100644 index 00000000000..69baa02e9b1 --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy @@ -0,0 +1,487 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "SmartSense Multi", namespace: "smartthings", author: "SmartThings") { + capability "Three Axis" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Signal Strength" + capability "Temperature Measurement" + capability "Sensor" + capability "Battery" + + fingerprint profileId: "FC01", deviceId: "0139" + } + + simulator { + status "open": "zone report :: type: 19 value: 0031" + status "closed": "zone report :: type: 19 value: 0030" + + status "acceleration": "acceleration: 1, rssi: 0, lqi: 0" + status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0" + + for (int i = 10; i <= 50; i += 10) { + status "temp ${i}C": "contactState: 0, accelerationState: 0, temp: $i C, battery: 100, rssi: 100, lqi: 255" + } + + // kinda hacky because it depends on how it is installed + status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255" + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + standardTile("acceleration", "device.acceleration") { + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + valueTile("temperature", "device.temperature") { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) { + state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + /* + valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { + state "lqi", label:'${currentValue}% signal', unit:"" + } + */ + + main(["contact", "acceleration", "temperature"]) + details(["contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/]) + } +} + +def parse(String description) { + def results + + if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + results = parseSingleMessage(description) + } + else if (description == 'updated') { + //TODO is there a better way to handle this like the other device types? + results = parseOtherMessage(description) + } + else { + results = parseMultiSensorMessage(description) + } + log.debug "Parse returned $results.descriptionText" + return results + +} + +private Map parseSingleMessage(description) { + + def name = parseName(description) + def value = parseValue(description) + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def handlerName = value == 'open' ? 'opened' : value + def isStateChange = isStateChange(device, name, value) + + def results = [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] + log.debug "Parse results for $device: $results" + + results +} + +//TODO this just to handle 'updated' for now - investigate better way to do this +private Map parseOtherMessage(description) { + def name = null + def value = description + def linkText = getLinkText(device) + def descriptionText = description + def handlerName = description + def isStateChange = isStateChange(device, name, value) + + def results = [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] + log.debug "Parse results for $device: $results" + + results +} + +private List parseMultiSensorMessage(description) { + def results = [] + if (isAccelerationMessage(description)) { + results = parseAccelerationMessage(description) + } + else if (isContactMessage(description)) { + results = parseContactMessage(description) + } + else if (isRssiLqiMessage(description)) { + results = parseRssiLqiMessage(description) + } + else if (isOrientationMessage(description)) { + results = parseOrientationMessage(description) + } + + results +} + +private List parseAccelerationMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('acceleration:')) { + results << getAccelerationResult(part, description) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private List parseContactMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('contactState:')) { + results << getContactResult(part, description) + } + else if (part.startsWith('accelerationState:')) { + results << getAccelerationResult(part, description) + } + else if (part.startsWith('temp:')) { + results << getTempResult(part, description) + } + else if (part.startsWith('battery:')) { + results << getBatteryResult(part, description) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private List parseOrientationMessage(String description) { + def results = [] + def xyzResults = [x: 0, y: 0, z: 0] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('x:')) { + def unsignedX = part.split(":")[1].trim().toInteger() + def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX + xyzResults.x = signedX + } + else if (part.startsWith('y:')) { + def unsignedY = part.split(":")[1].trim().toInteger() + def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY + xyzResults.y = signedY + } + else if (part.startsWith('z:')) { + def unsignedZ = part.split(":")[1].trim().toInteger() + def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ + xyzResults.z = signedZ + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results << getXyzResult(xyzResults, description) + + results +} + +private List parseRssiLqiMessage(String description) { + def results = [] + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('lastHopRssi:')) { + results << getRssiResult(part, description, true) + } + else if (part.startsWith('lastHopLqi:')) { + results << getLqiResult(part, description, true) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private getContactResult(part, description) { + def name = "contact" + def value = part.endsWith("1") ? "open" : "closed" + def handlerName = value == 'open' ? 'opened' : value + def linkText = getLinkText(device) + def descriptionText = "$linkText was $handlerName" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getAccelerationResult(part, description) { + def name = "acceleration" + def value = part.endsWith("1") ? "active" : "inactive" + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: value, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getTempResult(part, description) { + def name = "temperature" + def temperatureScale = getTemperatureScale() + def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value°$temperatureScale" + def isStateChange = isTemperatureStateChange(device, name, value.toString()) + + [ + name: name, + value: value, + unit: temperatureScale, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getXyzResult(results, description) { + def name = "threeAxis" + def value = "${results.x},${results.y},${results.z}" + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getBatteryResult(part, description) { + def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null + def name = "battery" + def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText Battery was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getRssiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopRssi" : "rssi" + def valueString = part.split(":")[1].trim() + def value = (Integer.parseInt(valueString) - 128).toString() + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value dBm" + if (lastHop) { + descriptionText += " on the last hop" + } + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: "dBm", + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +/** + * Use LQI (Link Quality Indicator) as a measure of signal strength. The values + * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal + * strength. Return as a percentage of 255. + * + * Note: To make the signal strength indicator more accurate, we could combine + * LQI with RSSI. + */ +private getLqiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopLqi" : "lqi" + def valueString = part.split(":")[1].trim() + def percentageOf = 255 + def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString() + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText Signal (LQI) was: ${value}${unit}" + if (lastHop) { + descriptionText += " on the last hop" + } + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +private Boolean isAccelerationMessage(String description) { + // "acceleration: 1, rssi: 91, lqi: 255" + description ==~ /acceleration:.*rssi:.*lqi:.*/ +} + +private Boolean isContactMessage(String description) { + // "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255" + description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/ +} + +private Boolean isRssiLqiMessage(String description) { + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/ +} + +private Boolean isOrientationMessage(String description) { + // "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255" + description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/ +} + +private String parseName(String description) { + if (isSupportedDescription(description)) { + return "contact" + } + null +} + +private String parseValue(String description) { + if (!isSupportedDescription(description)) { + return description + } + else if (zigbee.translateStatusZoneType19(description)) { + return "open" + } + else { + return "closed" + } +} + +private parseDescriptionText(String linkText, String value, String description) { + if (!isSupportedDescription(description)) { + return value + } + + value ? "$linkText was ${value == 'open' ? 'opened' : value}" : "" +} diff --git a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy new file mode 100644 index 00000000000..d2bc765264b --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy @@ -0,0 +1,352 @@ +/** + * SmartSense Open/Closed Accelerometer Sensor + * + * Copyright 2014 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: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Battery" + capability "Configuration" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Refresh" + capability "Temperature Measurement" + command "enrollResponse" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321" + } + + simulator { + + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + standardTile("acceleration", "device.acceleration") { + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["contact", "acceleration", "temperature"]) + details(["contact","acceleration", "temperature","battery","refresh"]) + } + } + + def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result + } + + private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0xFC02: + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap + } + + private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private int getHumidity(value) { + return Math.round(Double.parseDouble(value)) +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "FC02" && descMap.attrId == "0002") { + Integer.parseInt(descMap.value,8) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getContactResult('closed') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getContactResult('open') + break + + case '0x0022': // Tamper Alarm + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + resultMap = getContactResult('closed') + break + + case '0x0025': // Restore Report + resultMap = getContactResult('open') + break + + case '0x0026': // Trouble/Failure + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } + } + + private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def volts = rawValue / 10 + def descriptionText + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + + return result + } + + private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] + } + + private Map getContactResult(value) { + log.debug 'Contact Status' + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + return [ + name: 'contact', + value: value, + descriptionText: descriptionText + ] + } + + private getAccelerationResult(numValue) { + def name = "acceleration" + def value = numValue.endsWith("1") ? "active" : "inactive" + //def linkText = getLinkText(device) + def descriptionText = "$linkText was $value" + def isStateChange = isStateChange(device, name, value) + [ + name: name, + value: value, + //unit: null, + //linkText: linkText, + descriptionText: descriptionText, + //handlerName: value, + isStateChange: isStateChange + // displayed: displayed(description, isStateChange) + ] + } + + def refresh() + { + log.debug "Refreshing Temperature and Battery " + [ + + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + //"st rattr 0x${device.deviceNetworkId} 1 0xFC02 2", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] + } + + def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}" + + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy new file mode 100644 index 00000000000..513419ef41e --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -0,0 +1,326 @@ +/** + * SmartSense Open/Closed Sensor + * + * Copyright 2014 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: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Battery" + capability "Configuration" + capability "Contact Sensor" + capability "Refresh" + capability "Temperature Measurement" + + command "enrollResponse" + + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300" + } + + simulator { + + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["contact", "temperature"]) + details(["contact","temperature","battery","refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private int getHumidity(value) { + return Math.round(Double.parseDouble(value)) +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getContactResult('closed') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getContactResult('open') + break + + case '0x0022': // Tamper Alarm + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + resultMap = getContactResult('closed') + break + + case '0x0025': // Restore Report + resultMap = getContactResult('open') + break + + case '0x0026': // Trouble/Failure + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def volts = rawValue / 10 + def descriptionText + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + + return result +} + +private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getContactResult(value) { + log.debug 'Contact Status' + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + return [ + name: 'contact', + value: value, + descriptionText: descriptionText + ] +} + +def refresh() +{ + log.debug "Refreshing Temperature and Battery" + [ + + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + + //"raw 0x500 {01 23 00 00 00}", "delay 200", + //"send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}" + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/smartsense-presence.src/smartsense-presence.groovy b/devicetypes/smartthings/smartsense-presence.src/smartsense-presence.groovy new file mode 100644 index 00000000000..f413418fd68 --- /dev/null +++ b/devicetypes/smartthings/smartsense-presence.src/smartsense-presence.groovy @@ -0,0 +1,285 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "SmartSense Presence", namespace: "smartthings", author: "SmartThings") { + capability "Tone" + capability "Actuator" + capability "Signal Strength" + capability "Presence Sensor" + capability "Sensor" + capability "Battery" + + fingerprint profileId: "FC01", deviceId: "019A" + fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000,0003", outClusters: "0003" + fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000", outClusters: "0006" + } + + simulator { + status "present": "presence: 1" + status "not present": "presence: 0" + status "battery": "battery: 27, batteryDivisor: 0A, rssi: 100, lqi: 64" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0" + state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff" + } + standardTile("beep", "device.beep", decoration: "flat") { + state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff" + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + /* + valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { + state "lqi", label:'${currentValue}% signal', unit:"" + } + */ + + main "presence" + details(["presence", "beep", "battery"/*, "lqi"*/]) + } +} + +def beep() { + /* + You can make the speaker turn on for 0.5-second beeps by sending some CLI commands: + + Command: send raw, wait 7, send raw, wait 7, send raw + Future: new packet type "st.beep" + + raw 0xFC05 {15 0A 11 00 00 15 01} + send 0x2F7F 2 2 + + where "0xABCD" is the node ID of the Smart Tag, everything else above is a constant. Except + the "15 01" at the end of the first raw command, that sets the speaker's period (reciprocal + of frequency). You can play with this value up or down to experiment with loudness as the + loudness will be strongly dependent upon frequency and the enclosure that it's in. Note that + "15 01" represents the hex number 0x0115 so a lower frequency is "16 01" (longer period) and + a higher frequency is "14 01" (shorter period). Note that since the tag only checks its parent + for messages every 5 seconds (while at rest) or every 3 seconds (while in motion) it will take + up to this long from the time you send the message to the time you hear a sound. + */ + + [ + "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 7000", + "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 7000", + "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 7000", + "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 7000", + "raw 0xFC05 {15 0A 11 00 00 15 01}" + ] +} + +def parse(String description) { + def results + if (isBatteryMessage(description)) { + results = parseBatteryMessage(description) + } + else { + results = parsePresenceMessage(description) + } + + log.debug "Parse returned $results.descriptionText" + results +} + +private Map parsePresenceMessage(String description) { + def name = parseName(description) + def value = parseValue(description) + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def handlerName = getState(value) + def isStateChange = isStateChange(device, name, value) + + def results = [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] + + results +} + +private String parseName(String description) { + if (description?.startsWith("presence: ")) { + return "presence" + } + null +} + +private String parseValue(String description) { + if (description?.startsWith("presence: ")) + { + if (description?.endsWith("1")) + { + return "present" + } + else if (description?.endsWith("0")) + { + return "not present" + } + } + + description +} + +private parseDescriptionText(String linkText, String value, String description) { + switch(value) { + case "present": return "$linkText has arrived" + case "not present": return "$linkText has left" + default: return value + } +} + +private getState(String value) { + def state = value + if (value == "present") { + state = "arrived" + } + else if (value == "not present") { + state = "left" + } + + state +} + +private Boolean isBatteryMessage(String description) { + // "raw:36EF1C, dni:36EF, battery:1B, rssi:, lqi:" + description ==~ /.*battery:.*rssi:.*lqi:.*/ +} + +private List parseBatteryMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('battery:')) { + def batteryResult = getBatteryResult(part, description) + if (batteryResult) { + results << batteryResult + } + } + else if (part.startsWith('rssi:')) { + def rssiResult = getRssiResult(part, description) + if (rssiResult) { + results << rssiResult + } + } + else if (part.startsWith('lqi:')) { + def lqiResult = getLqiResult(part, description) + if (lqiResult) { + results << lqiResult + } + } + } + + results +} + +private getBatteryResult(part, description) { + def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null + def name = "battery" + def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText battery was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + //displayed: displayed(description, isStateChange) + displayed: false + ] +} + +private getRssiResult(part, description) { + def name = "rssi" + def parts = part.split(":") + if (parts.size() != 2) return null + + def valueString = parts[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + def value = (valueInt - 128).toString() + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value dBm" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: "dBm", + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + //displayed: displayed(description, isStateChange) + displayed: false + ] +} + +/** + * Use LQI (Link Quality Indicator) as a measure of signal strength. The values + * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal + * strength. Return as a percentage of 255. + * + * Note: To make the signal strength indicator more accurate, we could combine + * LQI with RSSI. + */ +private getLqiResult(part, description) { + def name = "lqi" + def parts = part.split(":") + if (parts.size() != 2) return null + + def valueString = parts[1].trim() + def valueInt = Integer.parseInt(valueString, 16) + def percentageOf = 255 + def value = Math.round((valueInt / percentageOf * 100)).toString() + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText Signal (LQI) was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + //displayed: displayed(description, isStateChange) + displayed: false + ] +} diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy new file mode 100644 index 00000000000..0132c668a60 --- /dev/null +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -0,0 +1,295 @@ +/** + * SmartSense Temp/Humidity Sensor + * + * Copyright 2014 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: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") { + capability "Configuration" + capability "Battery" + capability "Refresh" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + + fingerprint endpointId: "01", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003" + } + + simulator { + status 'H 40': 'catchall: 0104 FC45 01 01 0140 00 D9B9 00 04 C2DF 0A 01 000021780F' + status 'H 45': 'catchall: 0104 FC45 01 01 0140 00 D9B9 00 04 C2DF 0A 01 0000218911' + status 'H 57': 'catchall: 0104 FC45 01 01 0140 00 4E55 00 04 C2DF 0A 01 0000211316' + status 'H 53': 'catchall: 0104 FC45 01 01 0140 00 20CD 00 04 C2DF 0A 01 0000219814' + status 'H 43': 'read attr - raw: BF7601FC450C00000021A410, dni: BF76, endpoint: 01, cluster: FC45, size: 0C, attrId: 0000, result: success, encoding: 21, value: 10a4' + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("humidity", "device.humidity", inactiveLabel: false) { + state "humidity", label:'${currentValue}% humidity', unit:"" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery' + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "temperature", "humidity" + details(["temperature","humidity","battery","refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) { + map = parseCustomMessage(description) + } + + log.debug "Parse returned $map" + return map ? createEvent(map) : null +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0x0402: + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + + case 0xFC45: + String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('') + String display = Math.round(Integer.valueOf(pctStr, 16) / 100) + resultMap = getHumidityResult(display) + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + else if (descMap.cluster == "FC45" && descMap.attrId == "0000") { + def value = getReportAttributeHumidity(descMap.value) + resultMap = getHumidityResult(value) + } + + return resultMap +} + +def getReportAttributeHumidity(String value) { + def humidity = null + if (value?.trim()) { + try { + // value is hex with no decimal + def pct = Integer.parseInt(value.trim(), 16) / 100 + humidity = String.format('%.0f', pct) + } catch(NumberFormatException nfe) { + log.debug "Error converting $value to humidity" + } + } + return humidity +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + else if (description?.startsWith('humidity: ')) { + def pct = (description - "humidity: " - "%").trim() + if (pct.isNumber()) { + def value = Math.round(new BigDecimal(pct)).toString() + resultMap = getHumidityResult(value) + } else { + log.error "invalid humidity: ${pct}" + } + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def volts = rawValue / 10 + def descriptionText + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + + return result +} + +private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getHumidityResult(value) { + log.debug 'Humidity' + return [ + name: 'humidity', + value: value, + unit: '%' + ] +} + +def refresh() +{ + log.debug "refresh temperature, humidity, and battery" + [ + + "zcl mfg-code 0xC2DF", "delay 1000", + "zcl global read 0xFC45 0", "delay 1000", + "send 0x${device.deviceNetworkId} 1 1", "delay 1000", + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting and Bindings." + def configCmds = [ + + + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 1000", + + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}" + ] + return configCmds + refresh() // send refresh cmds as part of config +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy b/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy new file mode 100644 index 00000000000..c1c1e1e6c2a --- /dev/null +++ b/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy @@ -0,0 +1,412 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartSense Virtual OpenClosed + * + * Author: SmartThings + * Date: 2013-03-07 + */ +metadata { + definition (name: "SmartSense Virtual Open/Closed", namespace: "smartthings", author: "SmartThings") { + capability "Three Axis" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Signal Strength" + capability "Temperature Measurement" + capability "Sensor" + capability "Battery" + } + + simulator { + status "open": "zone report :: type: 19 value: 0031" + status "closed": "zone report :: type: 19 value: 0030" + + status "acceleration": "acceleration: 1, rssi: 0, lqi: 0" + status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0" + + for (int i = 20; i <= 100; i += 10) { + status "${i}F": "contactState: 0, accelerationState: 0, temp: $i F, battery: 100, rssi: 100, lqi: 255" + } + + // kinda hacky because it depends on how it is installed + status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255" + status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255" + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + standardTile("acceleration", "device.acceleration") { + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + } + valueTile("temperature", "device.temperature") { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) { + state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ + [value: 5, color: "#BC2323"], + [value: 10, color: "#D04E00"], + [value: 15, color: "#F1D801"], + [value: 16, color: "#FFFFFF"] + ]*/ + } + /* + valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { + state "lqi", label:'${currentValue}% signal', unit:"" + } + */ + + main(["contact", "acceleration", "temperature"]) + details(["contact", "acceleration", "temperature", "3axis", "battery"/*, "lqi"*/]) + } +} + +def parse(String description) { + def results + + if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + // Ignore this in favor of orientation-based state + // results = parseSingleMessage(description) + } + else { + results = parseMultiSensorMessage(description) + } + log.debug "Parse returned $results.descriptionText" + return results + +} + +private List parseMultiSensorMessage(description) { + def results = [] + if (isAccelerationMessage(description)) { + results = parseAccelerationMessage(description) + } + else if (isContactMessage(description)) { + results = parseContactMessage(description) + } + else if (isRssiLqiMessage(description)) { + results = parseRssiLqiMessage(description) + } + else if (isOrientationMessage(description)) { + results = parseOrientationMessage(description) + } + + results +} + +private List parseAccelerationMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('acceleration:')) { + results << getAccelerationResult(part, description) + } + /* + // TEMPORARILY THROW RSSI & LQI ON THE FLOOR TO SAVE PROCESSING + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + */ + } + + results +} + +private List parseContactMessage(String description) { + def results = [] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('accelerationState:')) { + results << getAccelerationResult(part, description) + } + else if (part.startsWith('temp:')) { + results << getTempResult(part, description) + } + else if (part.startsWith('battery:')) { + results << getBatteryResult(part, description) + } + /* + // TEMPORARILY THROW RSSI & LQI ON THE FLOOR TO SAVE PROCESSING + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + */ + } + + results +} + +private List parseOrientationMessage(String description) { + def results = [] + def xyzResults = [x: 0, y: 0, z: 0] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('x:')) { + def unsignedX = part.split(":")[1].trim().toInteger() + def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX + xyzResults.x = signedX + } + else if (part.startsWith('y:')) { + def unsignedY = part.split(":")[1].trim().toInteger() + def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY + xyzResults.y = signedY + } + else if (part.startsWith('z:')) { + def unsignedZ = part.split(":")[1].trim().toInteger() + def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ + xyzResults.z = signedZ + } + /* + // TEMPORARILY THROW RSSI & LQI ON THE FLOOR TO SAVE PROCESSING + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + */ + } + + def xyz = getXyzResult(xyzResults, description) + results << xyz + + // Looks for Z-axis orientation as virtual contact state + def a = xyz.value.split(',').collect{it.toInteger()} + def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1])) + def absValueZ = Math.abs(a[2]) + log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ" + + + if (absValueZ > 825 && absValueXY < 175) { + results << createEvent(name: "contact", value: "open", unit: "") + results << createEvent(name: "status", value: "open", unit: "") + log.debug "STATUS: open" + } + else if (absValueZ < 75 && absValueXY > 825) { + results << createEvent(name: "contact", value: "closed", unit: "") + results << createEvent(name: "status", value: "closed", unit: "") + log.debug "STATUS: closed" + } + + results +} + +private List parseRssiLqiMessage(String description) { + def results = [] + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('lastHopRssi:')) { + results << getRssiResult(part, description, true) + } + else if (part.startsWith('lastHopLqi:')) { + results << getLqiResult(part, description, true) + } + else if (part.startsWith('rssi:')) { + results << getRssiResult(part, description) + } + else if (part.startsWith('lqi:')) { + results << getLqiResult(part, description) + } + } + + results +} + +private getAccelerationResult(part, description) { + def name = "acceleration" + def value = part.endsWith("1") ? "active" : "inactive" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: value, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getTempResult(part, description) { + def name = "temperature" + def temperatureScale = getTemperatureScale() + def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def linkText = getLinkText(device) + def descriptionText = "$linkText was $value°$temperatureScale" + def isStateChange = isTemperatureStateChange(device, name, value.toString()) + + [ + name: name, + value: value, + unit: temperatureScale, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] +} + +private getXyzResult(results, description) { + def name = "threeAxis" + def value = "${results.x},${results.y},${results.z}" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getBatteryResult(part, description) { + def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null + def name = "battery" + def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was ${value}${unit}" + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: name, + isStateChange: isStateChange, + displayed: false + ] +} + +private getRssiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopRssi" : "rssi" + def valueString = part.split(":")[1].trim() + def value = (Integer.parseInt(valueString) - 128).toString() + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was $value dBm" + + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: "dBm", + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +/** + * Use LQI (Link Quality Indicator) as a measure of signal strength. The values + * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal + * strength. Return as a percentage of 255. + * + * Note: To make the signal strength indicator more accurate, we could combine + * LQI with RSSI. + */ +private getLqiResult(part, description, lastHop=false) { + def name = lastHop ? "lastHopLqi" : "lqi" + def valueString = part.split(":")[1].trim() + def percentageOf = 255 + def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString() + def unit = "%" + def linkText = getLinkText(device) + def descriptionText = "$linkText ${name} was: ${value}${unit}" + + def isStateChange = isStateChange(device, name, value) + + [ + name: name, + value: value, + unit: unit, + linkText: linkText, + descriptionText: descriptionText, + handlerName: null, + isStateChange: isStateChange, + displayed: false + ] +} + +private Boolean isAccelerationMessage(String description) { + // "acceleration: 1, rssi: 91, lqi: 255" + description ==~ /acceleration:.*rssi:.*lqi:.*/ +} + +private Boolean isContactMessage(String description) { + // "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255" + description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/ +} + +private Boolean isRssiLqiMessage(String description) { + // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" + description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/ +} + +private Boolean isOrientationMessage(String description) { + // "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255" + description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/ +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy new file mode 100644 index 00000000000..db07c46b72b --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy @@ -0,0 +1,352 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartWeather Station + * + * Author: SmartThings + * + * Date: 2013-04-30 + */ +metadata { + definition (name: "SmartWeather Station Tile", namespace: "smartthings", author: "SmartThings") { + capability "Illuminance Measurement" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + + attribute "localSunrise", "string" + attribute "localSunset", "string" + attribute "city", "string" + attribute "timeZoneOffset", "string" + attribute "weather", "string" + attribute "wind", "string" + attribute "weatherIcon", "string" + attribute "forecastIcon", "string" + attribute "feelsLike", "string" + attribute "percentPrecip", "string" + attribute "alert", "string" + attribute "alertKeys", "string" + attribute "sunriseDate", "string" + attribute "sunsetDate", "string" + + command "refresh" + } + + preferences { + input "zipCode", "text", title: "Zip Code (optional)", required: false + } + + tiles { + valueTile("temperature", "device.temperature") { + state "default", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + + valueTile("humidity", "device.humidity", decoration: "flat") { + state "default", label:'${currentValue}% humidity' + } + + standardTile("weatherIcon", "device.weatherIcon", decoration: "flat") { + state "chanceflurries", icon:"st.custom.wu1.chanceflurries", label: "" + state "chancerain", icon:"st.custom.wu1.chancerain", label: "" + state "chancesleet", icon:"st.custom.wu1.chancesleet", label: "" + state "chancesnow", icon:"st.custom.wu1.chancesnow", label: "" + state "chancetstorms", icon:"st.custom.wu1.chancetstorms", label: "" + state "clear", icon:"st.custom.wu1.clear", label: "" + state "cloudy", icon:"st.custom.wu1.cloudy", label: "" + state "flurries", icon:"st.custom.wu1.flurries", label: "" + state "fog", icon:"st.custom.wu1.fog", label: "" + state "hazy", icon:"st.custom.wu1.hazy", label: "" + state "mostlycloudy", icon:"st.custom.wu1.mostlycloudy", label: "" + state "mostlysunny", icon:"st.custom.wu1.mostlysunny", label: "" + state "partlycloudy", icon:"st.custom.wu1.partlycloudy", label: "" + state "partlysunny", icon:"st.custom.wu1.partlysunny", label: "" + state "rain", icon:"st.custom.wu1.rain", label: "" + state "sleet", icon:"st.custom.wu1.sleet", label: "" + state "snow", icon:"st.custom.wu1.snow", label: "" + state "sunny", icon:"st.custom.wu1.sunny", label: "" + state "tstorms", icon:"st.custom.wu1.tstorms", label: "" + state "cloudy", icon:"st.custom.wu1.cloudy", label: "" + state "partlycloudy", icon:"st.custom.wu1.partlycloudy", label: "" + state "nt_chanceflurries", icon:"st.custom.wu1.nt_chanceflurries", label: "" + state "nt_chancerain", icon:"st.custom.wu1.nt_chancerain", label: "" + state "nt_chancesleet", icon:"st.custom.wu1.nt_chancesleet", label: "" + state "nt_chancesnow", icon:"st.custom.wu1.nt_chancesnow", label: "" + state "nt_chancetstorms", icon:"st.custom.wu1.nt_chancetstorms", label: "" + state "nt_clear", icon:"st.custom.wu1.nt_clear", label: "" + state "nt_cloudy", icon:"st.custom.wu1.nt_cloudy", label: "" + state "nt_flurries", icon:"st.custom.wu1.nt_flurries", label: "" + state "nt_fog", icon:"st.custom.wu1.nt_fog", label: "" + state "nt_hazy", icon:"st.custom.wu1.nt_hazy", label: "" + state "nt_mostlycloudy", icon:"st.custom.wu1.nt_mostlycloudy", label: "" + state "nt_mostlysunny", icon:"st.custom.wu1.nt_mostlysunny", label: "" + state "nt_partlycloudy", icon:"st.custom.wu1.nt_partlycloudy", label: "" + state "nt_partlysunny", icon:"st.custom.wu1.nt_partlysunny", label: "" + state "nt_sleet", icon:"st.custom.wu1.nt_sleet", label: "" + state "nt_rain", icon:"st.custom.wu1.nt_rain", label: "" + state "nt_sleet", icon:"st.custom.wu1.nt_sleet", label: "" + state "nt_snow", icon:"st.custom.wu1.nt_snow", label: "" + state "nt_sunny", icon:"st.custom.wu1.nt_sunny", label: "" + state "nt_tstorms", icon:"st.custom.wu1.nt_tstorms", label: "" + state "nt_cloudy", icon:"st.custom.wu1.nt_cloudy", label: "" + state "nt_partlycloudy", icon:"st.custom.wu1.nt_partlycloudy", label: "" + } + valueTile("feelsLike", "device.feelsLike", decoration: "flat") { + state "default", label:'feels like ${currentValue}°' + } + + valueTile("wind", "device.wind", decoration: "flat") { + state "default", label:'wind ${currentValue} mph' + } + + valueTile("weather", "device.weather", decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("city", "device.city", decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("percentPrecip", "device.percentPrecip", decoration: "flat") { + state "default", label:'${currentValue}% precip' + } + + standardTile("refresh", "device.weather", decoration: "flat") { + state "default", label: "", action: "refresh", icon:"st.secondary.refresh" + } + + valueTile("alert", "device.alert", width: 3, height: 1, decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("rise", "device.localSunrise", decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("set", "device.localSunset", decoration: "flat") { + state "default", label:'${currentValue}' + } + + valueTile("light", "device.illuminance", decoration: "flat") { + state "default", label:'${currentValue} lux' + } + + main(["temperature", "weatherIcon","feelsLike"]) + details(["temperature", "humidity", "weatherIcon","feelsLike","wind","weather", "city","percentPrecip", "refresh","alert","rise","set","light"])} +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def installed() { + runPeriodically(3600, poll) +} + +def uninstalled() { + unschedule() +} + +// handle commands +def poll() { + log.debug "WUSTATION: Executing 'poll', location: ${location.name}" + + // Current conditions + def obs = get("conditions")?.current_observation + if (obs) { + def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0] + + if(getTemperatureScale() == "C") { + send(name: "temperature", value: Math.round(obs.temp_c), unit: "C") + send(name: "feelsLike", value: Math.round(obs.feelslike_c as Double), unit: "C") + } else { + send(name: "temperature", value: Math.round(obs.temp_f), unit: "F") + send(name: "feelsLike", value: Math.round(obs.feelslike_f as Double), unit: "F") + } + + send(name: "humidity", value: obs.relative_humidity[0..-2] as Integer, unit: "%") + send(name: "weather", value: obs.weather) + send(name: "weatherIcon", value: weatherIcon, displayed: false) + send(name: "wind", value: Math.round(obs.wind_mph) as String, unit: "MPH") // as String because of bug in determining state change of 0 numbers + + if (obs.local_tz_offset != device.currentValue("timeZoneOffset")) { + send(name: "timeZoneOffset", value: obs.local_tz_offset, isStateChange: true) + } + + def cityValue = "${obs.display_location.city}, ${obs.display_location.state}" + if (cityValue != device.currentValue("city")) { + send(name: "city", value: cityValue, isStateChange: true) + } + + // Sunrise / sunset + def a = get("astronomy")?.moon_phase + def today = localDate("GMT${obs.local_tz_offset}") + def ltf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm") + ltf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}")) + def utf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + utf.setTimeZone(TimeZone.getTimeZone("GMT")) + + def sunriseDate = ltf.parse("${today} ${a.sunrise.hour}:${a.sunrise.minute}") + def sunsetDate = ltf.parse("${today} ${a.sunset.hour}:${a.sunset.minute}") + + def tf = new java.text.SimpleDateFormat("h:mm a") + tf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}")) + def localSunrise = "${tf.format(sunriseDate)}" + def localSunset = "${tf.format(sunsetDate)}" + send(name: "localSunrise", value: localSunrise, descriptionText: "Sunrise today is at $localSunrise") + send(name: "localSunset", value: localSunset, descriptionText: "Sunset today at is $localSunset") + + send(name: "illuminance", value: estimateLux(sunriseDate, sunsetDate, weatherIcon)) + + // Forecast + def f = get("forecast") + def f1= f?.forecast?.simpleforecast?.forecastday + if (f1) { + def icon = f1[0].icon_url.split("/")[-1].split("\\.")[0] + def value = f1[0].pop as String // as String because of bug in determining state change of 0 numbers + send(name: "percentPrecip", value: value, unit: "%") + send(name: "forecastIcon", value: icon, displayed: false) + } + else { + log.warn "Forecast not found" + } + + // Alerts + def alerts = get("alerts")?.alerts + def newKeys = alerts?.collect{it.type + it.date_epoch} ?: [] + log.debug "WUSTATION: newKeys = $newKeys" + log.trace device.currentState("alertKeys") + def oldKeys = device.currentState("alertKeys")?.jsonValue + log.debug "WUSTATION: oldKeys = $oldKeys" + + def noneString = "no current weather alerts" + if (!newKeys && oldKeys == null) { + send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false) + send(name: "alert", value: noneString, descriptionText: "${device.displayName} has no current weather alerts", isStateChange: true) + } + else if (newKeys != oldKeys) { + if (oldKeys == null) { + oldKeys = [] + } + send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false) + + def newAlerts = false + alerts.each {alert -> + if (!oldKeys.contains(alert.type + alert.date_epoch)) { + def msg = "${alert.description} from ${alert.date} until ${alert.expires}" + send(name: "alert", value: pad(alert.description), descriptionText: msg, isStateChange: true) + newAlerts = true + } + } + + if (!newAlerts && device.currentValue("alert") != noneString) { + send(name: "alert", value: noneString, descriptionText: "${device.displayName} has no current weather alerts", isStateChange: true) + } + } + } + else { + log.warn "No response from Weather Underground API" + } +} + +def refresh() { + poll() +} + +def configure() { + poll() +} + +private pad(String s, size = 25) { + def n = (size - s.size()) / 2 + if (n > 0) { + def sb = "" + n.times {sb += " "} + sb += s + n.times {sb += " "} + return sb + } + else { + return s + } +} + + +private get(feature) { + getWeatherFeature(feature, zipCode) +} + +private localDate(timeZone) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + df.setTimeZone(TimeZone.getTimeZone(timeZone)) + df.format(new Date()) +} + +private send(map) { + log.debug "WUSTATION: event: $map" + sendEvent(map) +} + +private estimateLux(sunriseDate, sunsetDate, weatherIcon) { + def lux = 0 + def now = new Date().time + if (now > sunriseDate.time && now < sunsetDate.time) { + //day + switch(weatherIcon) { + case 'tstorms': + lux = 200 + break + case ['cloudy', 'fog', 'rain', 'sleet', 'snow', 'flurries', + 'chanceflurries', 'chancerain', 'chancesleet', + 'chancesnow', 'chancetstorms']: + lux = 1000 + break + case 'mostlycloudy': + lux = 2500 + break + case ['partlysunny', 'partlycloudy', 'hazy']: + lux = 7500 + break + default: + //sunny, clear + lux = 10000 + } + + //adjust for dusk/dawn + def afterSunrise = now - sunriseDate.time + def beforeSunset = sunsetDate.time - now + def oneHour = 1000 * 60 * 60 + + if(afterSunrise < oneHour) { + //dawn + lux = (long)(lux * (afterSunrise/oneHour)) + } else if (beforeSunset < oneHour) { + //dusk + lux = (long)(lux * (beforeSunset/oneHour)) + } + } + else { + //night - always set to 10 for now + //could do calculations for dusk/dawn too + lux = 10 + } + + lux +} diff --git a/devicetypes/smartthings/spark.src/spark.groovy b/devicetypes/smartthings/spark.src/spark.groovy new file mode 100644 index 00000000000..3f74ce4c13d --- /dev/null +++ b/devicetypes/smartthings/spark.src/spark.groovy @@ -0,0 +1,55 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Spark", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + } + + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + + main "switch" + details "switch" + } +} + +def parse(String description) { + log.error "This device does not support incoming events" + return null +} + +def on() { + put 'turnOn' +} + +def off() { + put 'turnOff' +} + +private put(action) { + // TODO: will this be configurable by user? + def apiKey = "fb91rfPFS84wmzH3" + + rest( + method: 'PUT', + endpoint: "http://sprk.io", + path: "/device/${device.deviceNetworkId}/${action}", + query: [api_key: apiKey] + ) +} diff --git a/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy new file mode 100644 index 00000000000..1c111a07195 --- /dev/null +++ b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy @@ -0,0 +1,121 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Configuration" + capability "Switch" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + main "switch" + details(["switch","levelSliderControl"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.info description + if (description?.startsWith("catchall:")) { + def msg = zigbee.parse(description) + log.trace msg + log.trace "data: $msg.data" + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } +} + +// Commands to device +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +} +def setLevel(value) { + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: value) + def level = hexString(Math.round(value * 255/100)) + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" + + //log.debug cmds + cmds +} + + +def configure() { + + /*log.debug "binding to switch and level control cluster" + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}" + ] + */ + + //set transition time to 2 seconds. Not currently working. + //"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}" +} + + + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} diff --git a/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy b/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy new file mode 100644 index 00000000000..16ed5824bd5 --- /dev/null +++ b/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy @@ -0,0 +1,87 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Temperature Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Sensor" + + fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0009,0402,0405" + } + + // simulator metadata + simulator { + for (int i = 0; i <= 100; i += 10) { + status "${i}F": "temperature: $i F" + } + + for (int i = 0; i <= 100; i += 10) { + status "${i}%": "humidity: ${i}%" + } + } + + // UI tile definitions + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("humidity", "device.humidity") { + state "humidity", label:'${currentValue}%', unit:"" + } + + main(["temperature", "humidity"]) + details(["temperature", "humidity"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def name = parseName(description) + def value = parseValue(description) + def unit = name == "temperature" ? getTemperatureScale() : (name == "humidity" ? "%" : null) + def result = createEvent(name: name, value: value, unit: unit) + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +private String parseName(String description) { + if (description?.startsWith("temperature: ")) { + return "temperature" + } else if (description?.startsWith("humidity: ")) { + return "humidity" + } + null +} + +private String parseValue(String description) { + if (description?.startsWith("temperature: ")) { + return zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + } else if (description?.startsWith("humidity: ")) { + def pct = (description - "humidity: " - "%").trim() + if (pct.isNumber()) { + return Math.round(new BigDecimal(pct)).toString() + } + } + null +} diff --git a/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy b/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy new file mode 100644 index 00000000000..edb6f149cda --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy @@ -0,0 +1,69 @@ +/** + * Simulated Alarm + * + * Copyright 2014 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 Alarm", namespace: "smartthings/testing", author: "SmartThings") { + capability "Alarm" + } + + simulator { + // reply messages + ["strobe","siren","both","off"].each { + reply "$it": "alarm:$it" + } + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label:'off', action:'alarm.both', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + state "strobe", label:'strobe!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + state "siren", label:'siren!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + standardTile("strobe", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.strobe", icon:"st.secondary.strobe", backgroundColor:"#cccccc" + } + standardTile("siren", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.siren", icon:"st.secondary.siren", backgroundColor:"#cccccc" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + } + main "alarm" + details(["alarm","strobe","siren","test","off"]) + } +} + +def strobe() { + sendEvent(name: "alarm", value: "strobe") +} + +def siren() { + sendEvent(name: "alarm", value: "siren") +} + +def both() { + sendEvent(name: "alarm", value: "both") +} + +def off() { + sendEvent(name: "alarm", value: "off") +} + +// Parse incoming device messages to generate events +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} diff --git a/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy b/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy new file mode 100644 index 00000000000..c0866e6a96f --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy @@ -0,0 +1,52 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Simulated Button", namespace: "smartthings/testing", author: "SmartThings") { + capability "Actuator" + capability "Button" + capability "Sensor" + + command "push1" + command "hold1" + } + + simulator { + + } + tiles { + standardTile("button", "device.button", width: 1, height: 1) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + standardTile("push1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 1", backgroundColor: "#ffffff", action: "push1" + } + standardTile("hold1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 1", backgroundColor: "#ffffff", action: "hold1" + } + main "button" + details(["button","push1","hold1"]) + } +} + +def parse(String description) { + +} + +def hold1() { + sendEvent(name: "button", value: "held", data: [buttonNumber: "1"], descriptionText: "$device.displayName button 1 was held", isStateChange: true) +} + +def push1() { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: "1"], descriptionText: "$device.displayName button 1 was pushed", isStateChange: true) +} diff --git a/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy b/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy new file mode 100644 index 00000000000..5f33a031a53 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy @@ -0,0 +1,39 @@ +metadata { + definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") { + capability "Color Control" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color" + } + valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { + state "saturation", label: 'Sat ${currentValue} ' + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } + main "rgbSelector" + details(["rgbSelector", "saturation", "hue"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + +} + +def setSaturation(percent) { + log.debug "Executing 'setSaturation'" + sendEvent(name: "saturation", value: percent) +} + +def setHue(percent) { + log.debug "Executing 'setHue'" + sendEvent(name: "hue", value: percent) +} diff --git a/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy b/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy new file mode 100644 index 00000000000..46d52f8754c --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy @@ -0,0 +1,51 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Contact Sensor", namespace: "smartthings/testing", author: "bob") { + capability "Contact Sensor" + + command "open" + command "close" + } + + simulator { + status "open": "contact:open" + status "closed": "contact:closed" + } + + 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") + } + main "contact" + details "contact" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def open() { + log.trace "open()" + sendEvent(name: "contact", value: "open") +} + +def close() { + log.trace "close()" + sendEvent(name: "contact", value: "closed") +} diff --git a/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy b/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy new file mode 100644 index 00000000000..a54c567bf2c --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy @@ -0,0 +1,72 @@ +/** + * Z-Wave Garage Door Opener + * + * Copyright 2014 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 Garage Door Opener", namespace: "smartthings/testing", author: "SmartThings") { + capability "Actuator" + capability "Door Control" + capability "Garage Door Control" + capability "Contact Sensor" + capability "Refresh" + capability "Sensor" + } + + simulator { + + } + + tiles { + standardTile("toggle", "device.door", width: 2, height: 2) { + state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening") + state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing") + state("opening", label:'${name}', icon:"st.doors.garage.garage-closed", backgroundColor:"#ffe71e") + state("closing", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#ffe71e") + + } + standardTile("open", "device.door", inactiveLabel: false, decoration: "flat") { + state "default", label:'open', action:"door control.open", icon:"st.doors.garage.garage-opening" + } + standardTile("close", "device.door", inactiveLabel: false, decoration: "flat") { + state "default", label:'close', action:"door control.close", icon:"st.doors.garage.garage-closing" + } + + main "toggle" + details(["toggle", "open", "close"]) + } +} + +def parse(String description) { + log.trace "parse($description)" +} + +def open() { + sendEvent(name: "door", value: "opening") + runIn(6, finishOpening) +} + +def close() { + sendEvent(name: "door", value: "closing") + runIn(6, finishClosing) +} + +def finishOpening() { + sendEvent(name: "door", value: "open") + sendEvent(name: "contact", value: "open") +} + +def finishClosing() { + sendEvent(name: "door", value: "closed") + sendEvent(name: "contact", value: "closed") +} diff --git a/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy b/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy new file mode 100644 index 00000000000..e94bec6530c --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy @@ -0,0 +1,52 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "bob") { + capability "Lock" + } + + // Simulated lock + tiles { + standardTile("toggle", "device.lock", width: 2, height: 2) { + state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821" + } + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked" + } + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked" + } + + main "toggle" + details(["toggle", "lock", "unlock"]) + } +} + +def parse(String description) { + log.trace "parse $description" + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def lock() { + log.trace "lock()" + sendEvent(name: "lock", value: "locked") +} + +def unlock() { + log.trace "unlock()" + sendEvent(name: "lock", value: "unlocked") +} diff --git a/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy b/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy new file mode 100644 index 00000000000..62057238b6e --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy @@ -0,0 +1,127 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Simulated Minimote", namespace: "smartthings/testing", author: "SmartThings") { + capability "Actuator" + capability "Button" + capability "Configuration" + capability "Sensor" + + command "push1" + command "push2" + command "push3" + command "push4" + command "hold1" + command "hold2" + command "hold3" + command "hold4" + } + + simulator { + status "button 1 pushed": "command: 2001, payload: 01" + status "button 1 held": "command: 2001, payload: 15" + status "button 2 pushed": "command: 2001, payload: 29" + status "button 2 held": "command: 2001, payload: 3D" + status "button 3 pushed": "command: 2001, payload: 51" + status "button 3 held": "command: 2001, payload: 65" + status "button 4 pushed": "command: 2001, payload: 79" + status "button 4 held": "command: 2001, payload: 8D" + status "wakeup": "command: 8407, payload: " + } + tiles { + standardTile("button", "device.button") { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + standardTile("push1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 1", backgroundColor: "#ffffff", action: "push1" + } + standardTile("push2", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 2", backgroundColor: "#ffffff", action: "push2" + } + standardTile("push3", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 3", backgroundColor: "#ffffff", action: "push3" + } + standardTile("push4", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 4", backgroundColor: "#ffffff", action: "push4" + } + standardTile("dummy1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: " ", backgroundColor: "#ffffff", action: "push4" + } + standardTile("hold1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 1", backgroundColor: "#ffffff", action: "hold1" + } + standardTile("hold2", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 2", backgroundColor: "#ffffff", action: "hold2" + } + standardTile("dummy2", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: " ", backgroundColor: "#ffffff", action: "push4" + } + standardTile("hold3", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 3", backgroundColor: "#ffffff", action: "hold3" + } + standardTile("hold4", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 4", backgroundColor: "#ffffff", action: "hold4" + } + + main "button" + details(["push1","push2","button","push3","push4","dummy1","hold1","hold2","dummy2","hold3","hold4"]) + } +} + +def parse(String description) { + +} + +def push1() { + push(1) +} + +def push2() { + push(2) +} + +def push3() { + push(3) +} + +def push4() { + push(4) +} + +def hold1() { + hold(1) +} + +def hold2() { + hold(2) +} + +def hold3() { + hold(3) +} + +def hold4() { + hold(4) +} + +private push(button) { + log.debug "$device.displayName button $button was pushed" + sendEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) +} + +private hold(button) { + log.debug "$device.displayName button $button was held" + sendEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) +} + diff --git a/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy b/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy new file mode 100644 index 00000000000..73ca0a9b416 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy @@ -0,0 +1,51 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Motion Sensor", namespace: "smartthings/testing", author: "bob") { + capability "Motion Sensor" + + command "active" + command "inactive" + } + + simulator { + status "active": "motion:active" + status "inactive": "motion:inactive" + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff", action: "active") + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0", action: "inactive") + } + main "motion" + details "motion" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def active() { + log.trace "active()" + sendEvent(name: "motion", value: "active") +} + +def inactive() { + log.trace "inactive()" + sendEvent(name: "motion", value: "inactive") +} diff --git a/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy b/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy new file mode 100644 index 00000000000..8b075da984c --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy @@ -0,0 +1,53 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Presence Sensor", namespace: "smartthings/testing", author: "bob") { + capability "Presence Sensor" + + command "arrived" + command "departed" + } + + simulator { + status "present": "presence: present" + status "not present": "presence: not present" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state("not present", label:'not present', icon:"st.presence.tile.not-present", backgroundColor:"#ffffff", action:"arrived") + state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#53a7c0", action:"departed") + } + main "presence" + details "presence" + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +// handle commands +def arrived() { + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "present") +} + + +def departed() { + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "not present") +} diff --git a/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy b/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy new file mode 100644 index 00000000000..c5bb05e4a34 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy @@ -0,0 +1,65 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Simulated Smoke Alarm", namespace: "smartthings/testing", author: "SmartThings") { + capability "Smoke Detector" + capability "Sensor" + + command "smoke" + command "test" + command "clear" + } + + simulator { + + } + + tiles { + standardTile("main", "device.smoke", width: 2, height: 2) { + state("clear", label:"Clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff", action:"smoke") + state("detected", label:"Smoke!", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13", action:"clear") + state("tested", label:"Test", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13", action:"clear") + } + standardTile("smoke", "device.smoke", inactiveLabel: false, decoration: "flat") { + state "default", label:'Smoke', action:"smoke" + } + standardTile("test", "device.smoke", inactiveLabel: false, decoration: "flat") { + state "default", label:'Test', action:"test" + } + standardTile("reset", "device.smoke", inactiveLabel: false, decoration: "flat") { + state "default", label:'Clear', action:"clear" + } + main "main" + details(["main", "smoke", "test", "clear"]) + } +} + +def parse(String description) { + +} + +def smoke() { + log.debug "smoke()" + sendEvent(name: "smoke", value: "detected", descriptionText: "$device.displayName smoke detected!") +} + +def test() { + log.debug "test()" + sendEvent(name: "smoke", value: "tested", descriptionText: "$device.displayName tested") +} + +def clear() { + log.debug "clear()" + sendEvent(name: "smoke", value: "clear", descriptionText: "$device.displayName clear") +} diff --git a/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy b/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy new file mode 100644 index 00000000000..e4256cc5f46 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy @@ -0,0 +1,67 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + + definition (name: "Simulated Switch", namespace: "smartthings/testing", author: "bob") { + capability "Switch" + capability "Relay Switch" + + command "onPhysical" + command "offPhysical" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + standardTile("on", "device.switch", decoration: "flat") { + state "default", label: 'On', action: "onPhysical", backgroundColor: "#ffffff" + } + standardTile("off", "device.switch", decoration: "flat") { + state "default", label: 'Off', action: "offPhysical", backgroundColor: "#ffffff" + } + main "switch" + details(["switch","on","off"]) + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def on() { + log.debug "$version on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "$version off()" + sendEvent(name: "switch", value: "off") +} + +def onPhysical() { + log.debug "$version onPhysical()" + sendEvent(name: "switch", value: "on", type: "physical") +} + +def offPhysical() { + log.debug "$version offPhysical()" + sendEvent(name: "switch", value: "off", type: "physical") +} + +private getVersion() { + "PUBLISHED" +} diff --git a/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy b/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy new file mode 100644 index 00000000000..6d328249e93 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy @@ -0,0 +1,76 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Temperature Sensor", namespace: "smartthings/testing", author: "SmartThings") { + capability "Temperature Measurement" + capability "Switch Level" + + command "up" + command "down" + command "setTemperature", ["number"] + } + + + // UI tile definitions + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}', unit:"F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + standardTile("up", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"up" + } + standardTile("down", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"down" + } + main "temperature" + details("temperature","up","down") + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"F") +} + +def setLevel(value) { + sendEvent(name:"temperature", value: value) +} + +def up() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue + 1 : 72 + sendEvent(name:"temperature", value: value) +} + +def down() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue - 1 : 72 + sendEvent(name:"temperature", value: value) +} + +def setTemperature(value) { + sendEvent(name:"temperature", value: value) +} diff --git a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy new file mode 100644 index 00000000000..5010ed82994 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy @@ -0,0 +1,255 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") { + capability "Thermostat" + + command "tempUp" + command "tempDown" + command "heatUp" + command "heatDown" + command "coolUp" + command "coolDown" + command "setTemperature", ["number"] + } + + tiles { + valueTile("temperature", "device.temperature", width: 1, height: 1) { + state("temperature", label:'${currentValue}', unit:"dF", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"tempDown" + } + standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"tempUp" + } + + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff" + } + standardTile("heatDown", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"heatDown" + } + standardTile("heatUp", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"heatUp" + } + + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("coolDown", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"coolDown" + } + standardTile("coolUp", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"coolUp" + } + + standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff" + state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e" + state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2" + state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821" + } + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff" + state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff" + state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff" + } + standardTile("operatingState", "device.thermostatOperatingState") { + state "idle", label:'${name}', backgroundColor:"#ffffff" + state "heating", label:'${name}', backgroundColor:"#ffa81e" + state "cooling", label:'${name}', backgroundColor:"#269bd2" + } + + main("temperature","operatingState") + details([ + "temperature","tempDown","tempUp", + "mode", "fanMode", "operatingState", + "heatingSetpoint", "heatDown", "heatUp", + "coolingSetpoint", "coolDown", "coolUp", + ]) + } +} + +def installed() { + sendEvent(name: "temperature", value: 72, unit: "F") + sendEvent(name: "heatingSetpoint", value: 70, unit: "F") + sendEvent(name: "thermostatSetpoint", value: 70, unit: "F") + sendEvent(name: "coolingSetpoint", value: 76, unit: "F") + sendEvent(name: "thermostatMode", value: "off") + sendEvent(name: "thermostatFanMode", value: "fanAuto") + sendEvent(name: "thermostatOperatingState", value: "idle") +} + +def parse(String description) { +} + +def evaluate(temp, heatingSetpoint, coolingSetpoint) { + log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint" + def threshold = 1.0 + def current = device.currentValue("thermostatOperatingState") + def mode = device.currentValue("thermostatMode") + + def heating = false + def cooling = false + def idle = false + if (mode in ["heat","emergency heat","auto"]) { + if (heatingSetpoint - temp >= threshold) { + heating = true + sendEvent(name: "thermostatOperatingState", value: "heating") + } + else if (temp - heatingSetpoint >= threshold) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (mode in ["cool","auto"]) { + if (temp - coolingSetpoint >= threshold) { + cooling = true + sendEvent(name: "thermostatOperatingState", value: "cooling") + } + else if (coolingSetpoint - temp >= threshold && !heating) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) + } + else { + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (idle && !heating && !cooling) { + sendEvent(name: "thermostatOperatingState", value: "idle") + } +} + +def setHeatingSetpoint(Double degreesF) { + log.debug "setHeatingSetpoint($degreesF)" + sendEvent(name: "heatingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint")) +} + +def setCoolingSetpoint(Double degreesF) { + log.debug "setCoolingSetpoint($degreesF)" + sendEvent(name: "coolingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF) +} + +def setThermostatMode(String value) { + sendEvent(name: "thermostatMode", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setThermostatFanMode(String value) { + sendEvent(name: "thermostatFanMode", value: value) +} + +def off() { + sendEvent(name: "thermostatMode", value: "off") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heat() { + sendEvent(name: "thermostatMode", value: "heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def auto() { + sendEvent(name: "thermostatMode", value: "auto") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def emergencyHeat() { + sendEvent(name: "thermostatMode", value: "emergency heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def cool() { + sendEvent(name: "thermostatMode", value: "cool") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def fanOn() { + sendEvent(name: "thermostatFanMode", value: "fanOn") +} + +def fanAuto() { + sendEvent(name: "thermostatFanMode", value: "fanAuto") +} + +def fanCirculate() { + sendEvent(name: "thermostatFanMode", value: "fanCirculate") +} + +def poll() { + null +} + +def tempUp() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue + 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def tempDown() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue - 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setTemperature(value) { + def ts = device.currentState("temperature") + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heatUp() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue + 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + +def heatDown() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue - 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + + +def coolUp() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue + 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} + +def coolDown() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue - 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} diff --git a/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy b/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy new file mode 100644 index 00000000000..74377fbb68b --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy @@ -0,0 +1,57 @@ +/** + * Copyright 2014 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 { + // Automatically generated. Make future change here. + definition (name: "Simulated Water Sensor", namespace: "smartthings/testing", author: "SmartThings") { + capability "Water Sensor" + + command "wet" + command "dry" + } + + simulator { + status "wet": "water:wet" + status "dry": "water:dry" + } + + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff", action: "wet" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0", action: "dry" + } + standardTile("wet", "device.water", inactiveLabel: false, decoration: "flat") { + state "default", label:'Wet', action:"wet", icon: "st.alarm.water.wet" + } + standardTile("dry", "device.water", inactiveLabel: false, decoration: "flat") { + state "default", label:'Dry', action:"dry", icon: "st.alarm.water.dry" + } + main "water" + details(["water","wet","dry"]) + } +} + +def parse(String description) { + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def wet() { + log.trace "wet()" + sendEvent(name: "water", value: "wet") +} + +def dry() { + log.trace "dry()" + sendEvent(name: "water", value: "dry") +} diff --git a/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy b/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy new file mode 100644 index 00000000000..478d7f62dfd --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy @@ -0,0 +1,46 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Simulated Water Valve", namespace: "smartthings/testing", author: "SmartThings") { + capability "Actuator" + capability "Valve" + capability "Sensor" + } + + // tile definitions + tiles { + standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { + state "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13" + state "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "contact" + details(["contact","refresh"]) + } +} + +def installed() { + sendEvent(name: "contact", value: "closed") +} + +def open() { + sendEvent(name: "contact", value: "open") +} + +def close() { + sendEvent(name: "contact", value: "closed") +} diff --git a/devicetypes/smartthings/thing.src/thing.groovy b/devicetypes/smartthings/thing.src/thing.groovy new file mode 100644 index 00000000000..4caa58162ba --- /dev/null +++ b/devicetypes/smartthings/thing.src/thing.groovy @@ -0,0 +1,39 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +//Thing is used while a device is in the process of being joined + +metadata { + definition (name: "Thing", namespace: "smartthings", author: "SmartThings") { + } + + // simulator metadata + simulator { + // Not Applicable to Thing Device + } + + // UI tile definitions + tiles { + standardTile("thing", "device.thing", width: 2, height: 2) { + state(name:"default", icon: "st.unknown.thing.thing-circle", label: "Please Wait") + } + + main "thing" + details "thing" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + // None +} diff --git a/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy b/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy new file mode 100644 index 00000000000..42ce1555bda --- /dev/null +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy @@ -0,0 +1,329 @@ +/** + * Tyco Door/Window Sensor + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +metadata { + definition (name: "Tyco Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Battery" + capability "Configuration" + capability "Contact Sensor" + capability "Refresh" + capability "Temperature Measurement" + + command "enrollResponse" + + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA" + } + + simulator { + + } + + preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + } + + valueTile("temperature", "device.temperature", inactiveLabel: false) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["contact", "temperature"]) + details(["contact","temperature","battery","refresh","configure"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : null + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + resultMap = getBatteryResult(cluster.data.last()) + break + + case 0x0402: + log.debug 'TEMP' + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } + log.debug "Desc Map: $descMap" + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + List parsedMsg = description.split(' ') + String msgCode = parsedMsg[2] + + Map resultMap = [:] + switch(msgCode) { + case '0x0020': // Closed/No Motion/Dry + resultMap = getContactResult('closed') + break + + case '0x0021': // Open/Motion/Wet + resultMap = getContactResult('open') + break + + case '0x0022': // Tamper Alarm + break + + case '0x0023': // Battery Alarm + break + + case '0x0024': // Supervision Report + resultMap = getContactResult('closed') + break + + case '0x0025': // Restore Report + resultMap = getContactResult('open') + break + + case '0x0026': // Trouble/Failure + break + + case '0x0028': // Test Mode + break + } + return resultMap +} + +def getTemperature(value) { + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return celsius + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def linkText = getLinkText(device) + + def result = [ + name: 'battery' + ] + + def volts = rawValue / 10 + def descriptionText + if (volts > 3.5) { + result.descriptionText = "${linkText} battery has too much power (${volts} volts)." + } + else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int) pct * 100) + result.descriptionText = "${linkText} battery was ${result.value}%" + } + + return result +} + +private Map getTemperatureResult(value) { + log.debug 'TEMP' + def linkText = getLinkText(device) + if (tempOffset) { + def offset = tempOffset as int + def v = value as int + value = v + offset + } + def descriptionText = "${linkText} was ${value}°${temperatureScale}" + return [ + name: 'temperature', + value: value, + descriptionText: descriptionText + ] +} + +private Map getContactResult(value) { + log.debug 'Contact Status' + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + return [ + name: 'contact', + value: value, + descriptionText: descriptionText + ] +} + +def refresh() +{ + log.debug "Refreshing Temperature and Battery" + [ + + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", + "st rattr 0x${device.deviceNetworkId} 1 1 0x20" + + ] +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting, IAS CIE, and Bindings." + def configCmds = [ + "delay 1000", + + "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + + //"raw 0x500 {01 23 00 00 00}", "delay 200", + //"send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + + "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", + "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", + + "delay 500" + ] + return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config +} + +def enrollResponse() { + log.debug "Sending enroll response" + [ + + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1" + + ] +} +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/unknown.src/unknown.groovy b/devicetypes/smartthings/unknown.src/unknown.groovy new file mode 100644 index 00000000000..e084bb15e4c --- /dev/null +++ b/devicetypes/smartthings/unknown.src/unknown.groovy @@ -0,0 +1,37 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Unknown", namespace: "smartthings", author: "SmartThings") { + } + + // simulator metadata + simulator { + // Not Applicable to Unknown Device + } + + // UI tile definitions + tiles { + standardTile("unknown", "device.unknown", width: 2, height: 2) { + state(name:"default", icon:"st.unknown.unknown.unknown", backgroundColor:"#767676", label: "Unknown") + } + + main "unknown" + details "unknown" + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + // None +} diff --git a/devicetypes/smartthings/wattvision.src/wattvision.groovy b/devicetypes/smartthings/wattvision.src/wattvision.groovy new file mode 100644 index 00000000000..936ded6083a --- /dev/null +++ b/devicetypes/smartthings/wattvision.src/wattvision.groovy @@ -0,0 +1,105 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Wattvision + * + * Author: steve + * Date: 2014-02-13 + */ +metadata { + + definition(name: "Wattvision", namespace: "smartthings", author: "Steve Vlaminck") { + capability "Power Meter" + capability "Refresh" + attribute "powerContent", "string" + } + + simulator { + // define status and reply messages here + } + + tiles { + + valueTile("power", "device.power") { + state "power", label: '${currentValue} W' + } + + tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { } + + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "power" + details(["powerChart", "power", "refresh"]) + + } +} + +def refresh() { + parent.getDataFromWattvision() + setGraphUrl(parent.getGraphUrl(device.deviceNetworkId)) +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +public setGraphUrl(graphUrl) { + + log.trace "setting url for Wattvision graph" + + sendEvent([ + date : new Date(), + value : graphUrl, + name : "powerContent", + displayed : false, + isStateChange : true, + description : "Graph updated", + descriptionText: "Graph updated" + ]) +} + +public addWattvisionData(json) { + + log.trace "Adding data from Wattvision" + + def data = json.data + def units = json.units ?: "watts" + + if (data) { + def latestData = data[-1] + data.each { + sendPowerEvent(it.t, it.v, units, (latestData == it)) + } + } + +} + +private sendPowerEvent(time, value, units, isLatest = false) { + def wattvisionDateFormat = parent.wattvisionDateFormat() + + def eventData = [ + date : new Date().parse(wattvisionDateFormat, time), + value : value, + name : "power", + displayed : isLatest, + isStateChange : isLatest, + description : "${value} ${units}", + descriptionText: "${value} ${units}" + ] + + log.debug "sending event: ${eventData}" + sendEvent(eventData) + +} diff --git a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy new file mode 100644 index 00000000000..4cd2ce0bfdd --- /dev/null +++ b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy @@ -0,0 +1,185 @@ +/** + * WeMo Direct LED Bulbs + * + * Copyright 2014 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. + * + * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart + * + */ +metadata { + definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") { + + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + + + main(["switch"]) + details(["switch", "level", "levelSliderControl", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.trace description + if (description?.startsWith("catchall:")) { + //def msg = zigbee.parse(description) + //log.trace msg + //log.trace "data: $msg.data" + if(description?.endsWith("0100")) + { + def result = createEvent(name: "switch", value: "on") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + if(description?.endsWith("0000")) + { + def result = createEvent(name: "switch", value: "off") + log.debug "Parse returned ${result?.descriptionText}" + return result + } + } + if (description?.startsWith("read attr")) { + log.debug description[-2..-1] + def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) + + sendEvent( name: "level", value: i ) + } + + +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} + +def refresh() { + [ + "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", + "st rattr 0x${device.deviceNetworkId} 1 8 0" + ] +} + +def setLevel(value) { + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {0000 0000}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: value) + def level = hexString(Math.round(value * 255/100)) + cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}" + + //log.debug cmds + cmds +} + +def configure() { + + String zigbeeId = swapEndianHex(device.hub.zigbeeId) + log.debug "Confuguring Reporting and Bindings." + def configCmds = [ + + //Switch Reporting + "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", + "send 0x${device.deviceNetworkId} 1 1", "delay 1000", + + //Level Control Reporting + "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", + "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", + ] + return configCmds + refresh() // send refresh cmds as part of config +} + + + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String swapEndianHex(String hex) { + reverseArray(hex.decodeHex()).encodeHex() +} + +private byte[] reverseArray(byte[] array) { + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array +} diff --git a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy new file mode 100644 index 00000000000..b5f9f5cc3fa --- /dev/null +++ b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy @@ -0,0 +1,276 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Wemo Light Switch + * + * Author: smartthings + * Date: 2014-02-14 + */ + + +metadata { + definition (name: "Wemo Light Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "subscribe" + command "resubscribe" + command "unsubscribe" + command "get" + } + + // simulator metadata + simulator {} + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" + state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details (["switch", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + + def msg = parseLanMessage(description) + def headerString = msg.header + + if (headerString?.contains("SID: uuid:")) { + def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" + sid -= "SID: uuid:".trim() + + updateDataValue("subscriptionId", sid) + } + + def result = [] + def bodyString = msg.body + if (bodyString) { + def body = new XmlSlurper().parseText(bodyString) + + if (body?.property?.TimeSyncRequest?.text()) { + log.trace "Got TimeSyncRequest" + result << timeSyncResponse() + } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { + log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" + } else if (body?.property?.BinaryState?.text()) { + def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" + log.trace "Notify: BinaryState = ${value}" + result << createEvent(name: "switch", value: value) + } else if (body?.property?.TimeZoneNotification?.text()) { + log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" + } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { + def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" + log.trace "GetBinaryResponse: BinaryState = ${value}" + result << createEvent(name: "switch", value: value) + } + } + + result +} + +//////////////////////////// +private getTime() { + // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. + ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() +} + +private getCallBackAddress() { + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress() { + def ip = getDataValue("ip") + def port = getDataValue("port") + + if (!ip || !port) { + def parts = device.deviceNetworkId.split(":") + if (parts.length == 2) { + ip = parts[0] + port = parts[1] + } else { + log.warn "Can't figure out ip and port for device: ${device.id}" + } + } + log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" + return convertHexToIP(ip) + ":" + convertHexToInt(port) +} + +//////////////////////////// +def on() { + log.debug "Executing 'on'" +def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" +Host: ${getHostAddress()} +Content-Type: text/xml +Content-Length: 333 + + + + + +1 + + +""", physicalgraph.device.Protocol.LAN) +} +//////////////////////////// +def off() { + log.debug "Executing 'off'" + + def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" +Host: ${getHostAddress()} +Content-Type: text/xml +Content-Length: 333 + + + + + +0 + + +""", physicalgraph.device.Protocol.LAN) +} + +//////////////////////////// +def refresh() { + log.debug "Executing WeMo Light Switch 'subscribe', then 'timeSyncResponse', then 'poll'" + [subscribe(), timeSyncResponse(), poll()] +} + +//////////////////////////// +def subscribe(hostAddress) { +log.debug "Executing 'subscribe()'" +def address = getCallBackAddress() +new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 +HOST: ${hostAddress} +CALLBACK: +NT: upnp:event +TIMEOUT: Second-5400 +User-Agent: CyberGarage-HTTP/1.0 + + +""", physicalgraph.device.Protocol.LAN) +} + +def subscribe() { + subscribe(getHostAddress()) +} + +def subscribe(ip, port) { + def existingIp = getDataValue("ip") + def existingPort = getDataValue("port") + if (ip && ip != existingIp) { + log.debug "Updating ip from $existingIp to $ip" + updateDataValue("ip", ip) + } + if (port && port != existingPort) { + log.debug "Updating port from $existingPort to $port" + updateDataValue("port", port) + } + + subscribe("${ip}:${port}") +} + +//////////////////////////// +def resubscribe() { +log.debug "Executing 'resubscribe()'" + +def sid = getDeviceDataByName("subscriptionId") + +new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 +HOST: ${getHostAddress()} +SID: uuid:${sid} +TIMEOUT: Second-5400 + + +""", physicalgraph.device.Protocol.LAN) + +} + +//////////////////////////// +def unsubscribe() { +def sid = getDeviceDataByName("subscriptionId") +new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 +HOST: ${getHostAddress()} +SID: uuid:${sid} + + +""", physicalgraph.device.Protocol.LAN) +} + +//////////////////////////// +//TODO: Use UTC Timezone +def timeSyncResponse() { +log.debug "Executing 'timeSyncResponse()'" +new physicalgraph.device.HubAction("""POST /upnp/control/timesync1 HTTP/1.1 +Content-Type: text/xml; charset="utf-8" +SOAPACTION: "urn:Belkin:service:timesync:1#TimeSync" +Content-Length: 375 +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + ${getTime()} + -05.00 + 1 + 1 + + + +""", physicalgraph.device.Protocol.LAN) +} + + +def poll() { +log.debug "Executing 'poll'" +new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" +Content-Length: 277 +Content-Type: text/xml; charset="utf-8" +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + + +""", physicalgraph.device.Protocol.LAN) +} diff --git a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy new file mode 100644 index 00000000000..eb3ea103520 --- /dev/null +++ b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy @@ -0,0 +1,228 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Wemo Motion + * + * Author: superuser + * Date: 2013-10-11 + */ + metadata { + definition (name: "Wemo Motion", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Refresh" + capability "Sensor" + + command "subscribe" + command "resubscribe" + command "unsubscribe" + } + + simulator { + // TODO: define status and reply messages here + } + + // UI tile definitions + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + } + standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "motion" + details (["motion", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + + def msg = parseLanMessage(description) + def headerString = msg.header + + if (headerString?.contains("SID: uuid:")) { + def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" + sid -= "SID: uuid:".trim() + + updateDataValue("subscriptionId", sid) + } + + def result = [] + def bodyString = msg.body + if (bodyString) { + def body = new XmlSlurper().parseText(bodyString) + + if (body?.property?.TimeSyncRequest?.text()) { + log.trace "Got TimeSyncRequest" + result << timeSyncResponse() + } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { + log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" + } else if (body?.property?.BinaryState?.text()) { + def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive" + log.debug "Notify - BinaryState = ${value}" + result << createEvent(name: "motion", value: value) + } else if (body?.property?.TimeZoneNotification?.text()) { + log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" + } + } + + result +} + +//////////////////////////// +private getTime() { + // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. + ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() +} + +private getCallBackAddress() { + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress() { + def ip = getDataValue("ip") + def port = getDataValue("port") + + if (!ip || !port) { + def parts = device.deviceNetworkId.split(":") + if (parts.length == 2) { + ip = parts[0] + port = parts[1] + } else { + log.warn "Can't figure out ip and port for device: ${device.id}" + } + } + log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" + return convertHexToIP(ip) + ":" + convertHexToInt(port) +} + +//////////////////////////// +def refresh() { + log.debug "Executing WeMo Motion 'subscribe', then 'timeSyncResponse', then 'getStatus'" + [subscribe(), timeSyncResponse(), getStatus()] +} + +//////////////////////////// +def getStatus() { +log.debug "Executing WeMo Motion 'getStatus'" +new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" +Content-Length: 277 +Content-Type: text/xml; charset="utf-8" +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + + +""", physicalgraph.device.Protocol.LAN) +} + +//////////////////////////// +def subscribe(hostAddress) { +log.debug "Executing 'subscribe()'" +def address = getCallBackAddress() +new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 +HOST: ${hostAddress} +CALLBACK: +NT: upnp:event +TIMEOUT: Second-4200 +User-Agent: CyberGarage-HTTP/1.0 + + +""", physicalgraph.device.Protocol.LAN) +} + +def subscribe() { + subscribe(getHostAddress()) +} + +def subscribe(ip, port) { + def existingIp = getDataValue("ip") + def existingPort = getDataValue("port") + if (ip && ip != existingIp) { + log.debug "Updating ip from $existingIp to $ip" + updateDataValue("ip", ip) + } + if (port && port != existingPort) { + log.debug "Updating port from $existingPort to $port" + updateDataValue("port", port) + } + + subscribe("${ip}:${port}") +} + +//////////////////////////// +def resubscribe() { +log.debug "Executing 'resubscribe()'" + +def sid = getDeviceDataByName("subscriptionId") + +new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 +HOST: ${getHostAddress()} +SID: uuid:${sid} +TIMEOUT: Second-4200 + + +""", physicalgraph.device.Protocol.LAN) + +} + +//////////////////////////// +def unsubscribe() { +def sid = getDeviceDataByName("subscriptionId") +new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 +HOST: ${getHostAddress()} +SID: uuid:${sid} + + +""", physicalgraph.device.Protocol.LAN) +} + +//////////////////////////// +//TODO: Use UTC Timezone +def timeSyncResponse() { +log.debug "Executing 'timeSyncResponse()'" +new physicalgraph.device.HubAction("""POST /upnp/control/timesync1 HTTP/1.1 +Content-Type: text/xml; charset="utf-8" +SOAPACTION: "urn:Belkin:service:timesync:1#TimeSync" +Content-Length: 376 +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + ${getTime()} + -05.00 + 1 + 1 + + + +""", physicalgraph.device.Protocol.LAN) +} diff --git a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy new file mode 100644 index 00000000000..b385ceb0b0a --- /dev/null +++ b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy @@ -0,0 +1,287 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Wemo Switch + * + * Author: superuser + * Date: 2013-10-11 + */ + metadata { + definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "subscribe" + command "resubscribe" + command "unsubscribe" + } + + // simulator metadata + simulator {} + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details (["switch", "refresh"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + + def msg = parseLanMessage(description) + def headerString = msg.header + + if (headerString?.contains("SID: uuid:")) { + def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" + sid -= "SID: uuid:".trim() + + updateDataValue("subscriptionId", sid) + } + + def result = [] + def bodyString = msg.body + if (bodyString) { + def body = new XmlSlurper().parseText(bodyString) + + if (body?.property?.TimeSyncRequest?.text()) { + log.trace "Got TimeSyncRequest" + result << timeSyncResponse() + } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { + log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" + } else if (body?.property?.BinaryState?.text()) { + def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" + log.trace "Notify: BinaryState = ${value}" + result << createEvent(name: "switch", value: value) + } else if (body?.property?.TimeZoneNotification?.text()) { + log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" + } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { + def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" + log.trace "GetBinaryResponse: BinaryState = ${value}" + result << createEvent(name: "switch", value: value) + } + } + + result +} + +private getTime() { + // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. + ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() +} + +private getCallBackAddress() { + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress() { + def ip = getDataValue("ip") + def port = getDataValue("port") + + if (!ip || !port) { + def parts = device.deviceNetworkId.split(":") + if (parts.length == 2) { + ip = parts[0] + port = parts[1] + } else { + log.warn "Can't figure out ip and port for device: ${device.id}" + } + } + log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" + return convertHexToIP(ip) + ":" + convertHexToInt(port) +} + + +def on() { + log.debug "Executing 'on'" + sendEvent(name: "switch", value: "on") +def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" +Host: ${getHostAddress()} +Content-Type: text/xml +Content-Length: 333 + + + + + +1 + + +""", physicalgraph.device.Protocol.LAN) +} + +def off() { + log.debug "Executing 'off'" + sendEvent(name: "switch", value: "off") + def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" +Host: ${getHostAddress()} +Content-Type: text/xml +Content-Length: 333 + + + + + +0 + + +""", physicalgraph.device.Protocol.LAN) +} + +/*def refresh() { + log.debug "Executing 'refresh'" +new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" +Content-Length: 277 +Content-Type: text/xml; charset="utf-8" +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + + +""", physicalgraph.device.Protocol.LAN) +}*/ + +def refresh() { + log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" + [subscribe(), timeSyncResponse(), poll()] +} + +def subscribe(hostAddress) { +log.debug "Executing 'subscribe()'" +def address = getCallBackAddress() +new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 +HOST: ${hostAddress} +CALLBACK: +NT: upnp:event +TIMEOUT: Second-5400 +User-Agent: CyberGarage-HTTP/1.0 + + +""", physicalgraph.device.Protocol.LAN) +} + +def subscribe() { + subscribe(getHostAddress()) +} + +def subscribe(ip, port) { + def existingIp = getDataValue("ip") + def existingPort = getDataValue("port") + if (ip && ip != existingIp) { + log.debug "Updating ip from $existingIp to $ip" + updateDataValue("ip", ip) + } + if (port && port != existingPort) { + log.debug "Updating port from $existingPort to $port" + updateDataValue("port", port) + } + + subscribe("${ip}:${port}") +} + +//////////////////////////// +def resubscribe() { +log.debug "Executing 'resubscribe()'" + +def sid = getDeviceDataByName("subscriptionId") + +new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 +HOST: ${getHostAddress()} +SID: uuid:${sid} +TIMEOUT: Second-5400 + + +""", physicalgraph.device.Protocol.LAN) + +} + +//////////////////////////// +def unsubscribe() { +def sid = getDeviceDataByName("subscriptionId") +new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 +HOST: ${getHostAddress()} +SID: uuid:${sid} + + +""", physicalgraph.device.Protocol.LAN) +} + +//////////////////////////// +//TODO: Use UTC Timezone +def timeSyncResponse() { +log.debug "Executing 'timeSyncResponse()'" +new physicalgraph.device.HubAction("""POST /upnp/control/timesync1 HTTP/1.1 +Content-Type: text/xml; charset="utf-8" +SOAPACTION: "urn:Belkin:service:timesync:1#TimeSync" +Content-Length: 376 +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + ${getTime()} + -05.00 + 1 + 1 + + + +""", physicalgraph.device.Protocol.LAN) +} + + +def poll() { +log.debug "Executing 'poll'" +new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" +Content-Length: 277 +Content-Type: text/xml; charset="utf-8" +HOST: ${getHostAddress()} +User-Agent: CyberGarage-HTTP/1.0 + + + + + + + +""", physicalgraph.device.Protocol.LAN) +} diff --git a/devicetypes/smartthings/wireless-scale.src/wireless-scale.groovy b/devicetypes/smartthings/wireless-scale.src/wireless-scale.groovy new file mode 100644 index 00000000000..5d6bb23b57d --- /dev/null +++ b/devicetypes/smartthings/wireless-scale.src/wireless-scale.groovy @@ -0,0 +1,189 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Smart Body Analyzer + * + * Author: SmartThings + * Date: 2013-09-27 + */ +metadata { + definition (name: "Wireless Scale", namespace: "smartthings", author: "SmartThings") { + capability "Polling" + + attribute "weight", "string" + attribute "leanMass", "string" + attribute "fatRatio", "string" + attribute "fatMass", "string" + + command "storeGraphImage" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { + state "default", label: "Withings", action: "", icon: "st.Bath.bath2", backgroundColor: "#FFFFFF" + } + + plotTile("weightGraph", "device.weight", "scatter", width:2, height:1, content:"weightPoints") + valueTile("weight", "device.weight", width: 1, height: 1) { + state("weight", label:'${currentValue} lbs', unit:"lbs", inactiveLabel: false) + } + + plotTile("fatGraph", "device.fatRatio", "scatter", width:2, height:1, content:"fatPoints") + valueTile("fatRatio", "device.fatRatio", width: 1, height: 1) { + state("fatRatio", label:'${currentValue}% bodyfat', unit:"bodyfat", inactiveLabel: false) + } + + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"polling.poll", icon:"st.secondary.refresh" + } + + main "icon" + details(["weightGraph", "weight", "fatGraph", "fatRatio", "refresh"]) + } +} + +mappings { + path("/weightPoints") { + action: [ + GET: "weightPoints" + ] + } + path("/fatPoints") { + action: [ + GET: "fatPoints" + ] + } +} + +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def parse(Map event) { + log.debug "Parsing '${event}'" + + if(event.status == 200 && event.data) + { + parent.parse(event) + return null + } + else if(["weight", "leanMass", "fatRatio", "fatMass"].contains(event.name) && event.date) + { + def dateString = event.date + data["measure.${event.name}.$dateString"] = event.value + + def old = "measure.${event.name}." + (new Date() - 30).format('yyyy-MM-dd') + data.findAll { it.key.startsWith("measure.${event.name}.") && it.key < old }.collect { it.key }.each { state.remove(it) } + } + + return event +} + +def storeGraphImage(String name, ByteArrayInputStream is, String contentType) { + storeImage(name, is, contentType) +} + +def poll() { + log.debug "Executing 'poll'" + parent.poll() +} + +def refresh() { + log.debug "Executing 'refresh'" + sendEvent(name:"refreshing", description:"Refreshing Withings data", displayed:true) + + return null +} + +def measurementPoints(name) { + def points = [] + + def wdata = normalizeMeasurementPoints(name) + log.debug "data: ${wdata}" + def allValues = wdata.collect { it.y } + log.debug "allValues: ${allValues}" + + def min = allValues.min() + def max = allValues.max() + + def minMax = [:] + minMax[min] = min + minMax[max] = max + log.debug "minMax: $minMax" + + wdata.reverse().each { it -> + points << plotPoint(it.x, it.y, minMax) + } + log.debug "points: ${points}" + + return points.reverse() +} + +private normalizeMeasurementPoints(name) { + def measurementData = data.findAll { it.key.startsWith("measure.${name}.") } + log.debug "measurementData: ${measurementData}" + + def normalizedData = [] + measurementData.each { k, v -> + def d = Date.parse('yyyy-MM-dd', k - "measure.${name}.") + Calendar cal = Calendar.getInstance(); + cal.setTime(d) + // BUG: DOES NOT HANDLE NEW YEAR PROPERLY + // Should concat YEAR + (PAD_LEFT(DAY_OF_YEAR)) + // 2013365 == Dec. 31, 2013 + // 2014001 == Jan. 1, 2014 + normalizedData << [x:cal.get(Calendar.DAY_OF_YEAR), y:v] + } + log.debug "normalizedData: ${normalizedData}" + + normalizedData.sort{ it.x } +} + +private plotPoint(x, y, minMax) { + def removed = minMax.remove(y) != null + + return [ + color:"", + fillColor: removed ? "" : "#f3f3f3", + symbolStyle:"elipse", + point:y, + label: removed ? "${y}" : "", + x:x, + y:y + ] +} + +def weightPoints() { + return [ + "title":"My Weight", + "plots":[ + "weight":[ + "points":measurementPoints("weight") + ] + ] + ] +} + +def fatPoints() { + return [ + "title":"Bodyfat %", + "plots":[ + "fat":[ + "points":measurementPoints("fatRatio") + ] + ] + ] +} diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy new file mode 100644 index 00000000000..e14b5508e1f --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -0,0 +1,142 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Switch" + capability "Configuration" + capability "Sensor" + capability "Refresh" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff" + } + main "switch" + details(["switch", "refresh", "level", "levelSliderControl"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.info description + if (description?.startsWith("catchall:")) { + def msg = zigbee.parse(description) + log.trace msg + log.trace "data: $msg.data" + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } +} + +// Commands to device +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +} +def setLevel(value) { + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + + } + + sendEvent(name: "level", value: value) + def level = hexString(Math.round(value * 255/100)) + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" + + //log.debug cmds + cmds +} + +def refresh() { + [ + "st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200", + "st wattr 0x${device.deviceNetworkId} 1 8 0" + ] +} + +def configure() { + + /*log.debug "binding to switch and level control cluster" + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", + "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}" + ] + */ + + //set transition time to 2 seconds. Not currently working. + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}" +} + + + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} diff --git a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy new file mode 100644 index 00000000000..a4788277950 --- /dev/null +++ b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy @@ -0,0 +1,240 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +/* Philips Hue (via Zigbee) + +Capabilities: + Actuator + Color Control + Configuration + Polling + Refresh + Sensor + Switch + Switch Level + +Custom Commands: + setAdjustedColor + +*/ + +metadata { + definition (name: "Zigbee Hue Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Switch" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Sensor" + + command "setAdjustedColor" + + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { + state "color", action:"setAdjustedColor" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { + state "saturation", action:"color control.setSaturation" + } + valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { + state "saturation", label: 'Sat ${currentValue} ' + } + controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { + state "hue", action:"color control.setHue" + } + + main(["switch"]) + details(["switch", "levelSliderControl", "rgbSelector", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + //log.trace description + if (description?.startsWith("catchall:")) { + def msg = zigbee.parse(description) + //log.trace msg + //log.trace "data: $msg.data" + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } +} + +def on() { + // just assume it works for now + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" +} + +def off() { + // just assume it works for now + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +} + +def setHue(value) { + def max = 0xfe + log.trace "setHue($value)" + sendEvent(name: "hue", value: value) + def scaledValue = Math.round(value * max / 100.0) + def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledValue)} 00 0000}" + //log.info cmd + cmd +} + +def setAdjustedColor(value) { + log.debug "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.hue = adjustOutgoingHue(value.hue) + adjusted.level = null // needed because color picker always sends 100 + setColor(adjusted) +} + +def setColor(value){ + log.trace "setColor($value)" + def max = 0xfe + + sendEvent(name: "hue", value: value.hue) + sendEvent(name: "saturation", value: value.saturation) + def scaledHueValue = Math.round(value.hue * max / 100.0) + def scaledSatValue = Math.round(value.saturation * max / 100.0) + + def cmd = [] + if (value.switch != "off" && device.latestValue("switch") == "off") { + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" + cmd << "delay 150" + } + + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledHueValue)} 00 0000}" + cmd << "delay 150" + cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledSatValue)} 0000}" + + if (value.level != null) { + cmd << "delay 150" + cmd.addAll(setLevel(value.level)) + } + + if (value.switch == "off") { + cmd << "delay 150" + cmd << off() + } + log.info cmd + cmd +} + +def setSaturation(value) { + def max = 0xfe + log.trace "setSaturation($value)" + sendEvent(name: "saturation", value: value) + def scaledValue = Math.round(value * max / 100.0) + def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledValue)} 0000}" + //log.info cmd + cmd +} + +def refresh() { + "st rattr 0x${device.deviceNetworkId} 1 6 0" +} + +def poll(){ + log.debug "Poll is calling refresh" + refresh() +} + +def setLevel(value) { + log.trace "setLevel($value)" + def cmds = [] + + if (value == 0) { + sendEvent(name: "switch", value: "off") + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" + } + else if (device.latestValue("switch") == "off") { + sendEvent(name: "switch", value: "on") + } + + sendEvent(name: "level", value: value) + def level = hexString(Math.round(value * 255/100)) + cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" + + //log.debug cmds + cmds +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} + +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} + +private adjustOutgoingHue(percent) { + def adjusted = percent + if (percent > 31) { + if (percent < 63.0) { + adjusted = percent + (7 * (percent -30 ) / 32) + } + else if (percent < 73.0) { + adjusted = 69 + (5 * (percent - 62) / 10) + } + else { + adjusted = percent + (2 * (100 - percent) / 28) + } + } + log.info "percent: $percent, adjusted: $adjusted" + adjusted +} diff --git a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy new file mode 100644 index 00000000000..25a4dd99194 --- /dev/null +++ b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy @@ -0,0 +1,110 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +/* + * Capabilities + * - Battery + * - Configuration + * - Refresh + * - Switch + * - Valve +*/ + +metadata { + definition (name: "Zigbee Valve", namespace: "smartthings", author: "SmartThings") { + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Valve" + + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0020,0006,0B02", outClusters: "0003" + } + + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + // UI tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: 'closed', action: "switch.on", icon: "st.Outdoor.outdoor16", backgroundColor: "#e86d13" + state "on", label: 'open', action: "switch.off", icon: "st.Outdoor.outdoor16", backgroundColor: "#53a7c0" + } + main "switch" + details(["switch"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.info description + if (description?.startsWith("catchall:")) { + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + def msg = zigbee.parse(description) + log.debug "Parse returned ${result?.descriptionText}" + return result + log.trace msg + log.trace "data: $msg.data" + } + else { + def name = description?.startsWith("on/off: ") ? "switch" : null + def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null + def result = createEvent(name: name, value: value) + log.debug "Parse returned ${result?.descriptionText}" + return result + } +} + +// Commands to device +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} + +def open() { + log.debug "on()" + sendEvent(name: "switch", value: "on") + "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" +} + +def close() { + log.debug "off()" + sendEvent(name: "switch", value: "off") + "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +} + +def refresh() { + log.debug "sending refresh command" + "st rattr 0x${device.deviceNetworkId} 1 6 0" +} + +def configure() { + + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}" +} diff --git a/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy b/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy new file mode 100644 index 00000000000..2d009afd27a --- /dev/null +++ b/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy @@ -0,0 +1,76 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Controller", namespace: "smartthings", author: "SmartThings") { + + command "on" + command "off" + + fingerprint deviceId: "0x02" + } + + simulator { + + } + + tiles { + standardTile("state", "device.state", width: 2, height: 2) { + state 'connected', icon: "st.unknown.zwave.static-controller", backgroundColor:"#ffffff" + } + standardTile("basicOn", "device.switch", inactiveLabel:false, decoration:"flat") { + state "on", label:"on", action:"on", icon:"st.switches.switch.on" + } + standardTile("basicOff", "device.switch", inactiveLabel: false, decoration:"flat") { + state "off", label:"off", action:"off", icon:"st.switches.switch.off" + } + + main "state" + details(["state", "basicOn", "basicOff"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def event = [isStateChange: true] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: ${cmd.encapsulatedCommand()} [secure]" + event +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def event = [isStateChange: true] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: $cmd" + event +} + +def on() { + zwave.basicV1.basicSet(value: 0xFF).format() +} + +def off() { + zwave.basicV1.basicSet(value: 0x00).format() +} diff --git a/devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy b/devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy new file mode 100644 index 00000000000..14b05d01a1d --- /dev/null +++ b/devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy @@ -0,0 +1,256 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Device Multichannel", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Refresh" + capability "Configuration" + capability "Sensor" + capability "Zw Multichannel" + + fingerprint inClusters: "0x60" + fingerprint inClusters: "0x60, 0x25" + fingerprint inClusters: "0x60, 0x26" + fingerprint inClusters: "0x5E, 0x59, 0x60, 0x8E" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + reply "8E010101,delay 800,6007": "command: 6008, payload: 4004" + reply "8505": "command: 8506, payload: 02" + reply "59034002": "command: 5904, payload: 8102003101000000" + reply "6007": "command: 6008, payload: 0002" + reply "600901": "command: 600A, payload: 10002532" + reply "600902": "command: 600A, payload: 210031" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.unknown.zwave.device", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.unknown.zwave.device", backgroundColor: "#ffffff" + } + standardTile("switchOn", "device.switch", inactiveLabel: false, decoration: "flat") { + state "on", label:'on', action:"switch.on", icon:"st.switches.switch.on" + } + standardTile("switchOff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "off", label:'off', action:"switch.off", icon:"st.switches.switch.off" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main "switch" + details (["switch", "switchOn", "switchOff", "levelSliderControl", "refresh"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug("'$description' parsed to $result") + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + [ createEvent(descriptionText: "${device.displayName} woke up", isStateChange:true), + response(["delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) ] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + if (cmd.value == 0) { + createEvent(name: "switch", value: "off") + } else if (cmd.value == 255) { + createEvent(name: "switch", value: "on") + } else { + [ createEvent(name: "switch", value: "on"), createEvent(name: "switchLevel", value: cmd.value) ] + } +} + +private List loadEndpointInfo() { + if (state.endpointInfo) { + state.endpointInfo + } else if (device.currentValue("epInfo")) { + fromJson(device.currentValue("epInfo")) + } else { + [] + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd) { + updateDataValue("endpoints", cmd.endPoints.toString()) + if (!state.endpointInfo) { + state.endpointInfo = loadEndpointInfo() + } + if (state.endpointInfo.size() > cmd.endPoints) { + cmd.endpointInfo + } + state.endpointInfo = [null] * cmd.endPoints + //response(zwave.associationV2.associationGroupingsGet()) + [ createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:""), + response(zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: 1)) ] +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) { + def result = [] + def cmds = [] + if(!state.endpointInfo) state.endpointInfo = [] + state.endpointInfo[cmd.endPoint - 1] = cmd.format()[6..-1] + if (cmd.endPoint < getDataValue("endpoints").toInteger()) { + cmds = zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: cmd.endPoint + 1).format() + } else { + log.debug "endpointInfo: ${state.endpointInfo.inspect()}" + } + result << createEvent(name: "epInfo", value: util.toJson(state.endpointInfo), displayed: false, descriptionText:"") + if(cmds) result << response(cmds) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationGroupingsReport cmd) { + state.groups = cmd.supportedGroupings + if (cmd.supportedGroupings > 1) { + [response(zwave.associationGrpInfoV1.associationGroupInfoGet(groupingIdentifier:2, listMode:1))] + } +} + +def zwaveEvent(physicalgraph.zwave.commands.associationgrpinfov1.AssociationGroupInfoReport cmd) { + def cmds = [] + /*for (def i = 0; i < cmd.groupCount; i++) { + def prof = cmd.payload[5 + (i * 7)] + def num = cmd.payload[3 + (i * 7)] + if (prof == 0x20 || prof == 0x31 || prof == 0x71) { + updateDataValue("agi$num", String.format("%02X%02X", *(cmd.payload[(7*i+5)..(7*i+6)]))) + cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:num, nodeId:zwaveHubNodeId)) + } + }*/ + for (def i = 2; i <= state.groups; i++) { + cmds << response(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:i, nodeId:zwaveHubNodeId)) + } + cmds +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + if (encapsulatedCommand) { + if (state.enabledEndpoints.find { it == cmd.sourceEndPoint }) { + def formatCmd = ([cmd.commandClass, cmd.command] + cmd.parameter).collect{ String.format("%02X", it) }.join() + createEvent(name: "epEvent", value: "$cmd.sourceEndPoint:$formatCmd", isStateChange: true, displayed: false, descriptionText: "(fwd to ep $cmd.sourceEndPoint)") + } else { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof physicalgraph.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + // def encapsulatedCommand = cmd.encapsulatedCommand(versions) + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", isStateChange: true) +} + +def on() { + commands([zwave.basicV1.basicSet(value: 0xFF), zwave.basicV1.basicGet()]) +} + +def off() { + commands([zwave.basicV1.basicSet(value: 0x00), zwave.basicV1.basicGet()]) +} + +def refresh() { + command(zwave.basicV1.basicGet()) +} + +def setLevel(value) { + commands([zwave.basicV1.basicSet(value: value as Integer), zwave.basicV1.basicGet()], 4000) +} + +def configure() { + commands([ + zwave.multiChannelV3.multiChannelEndPointGet() + ], 800) +} + +def epCmd(Integer ep, String cmds) { + def result + if (cmds) { + def header = state.sec ? "988100600D00" : "600D00" + result = cmds.split(",").collect { cmd -> (cmd.startsWith("delay")) ? cmd : String.format("%s%02X%s", header, ep, cmd) } + } + result +} + +def enableEpEvents(enabledEndpoints) { + state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger() + null +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +private encap(cmd, endpoint) { + if (endpoint) { + command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd)) + } else { + command(cmd) + } +} + +private encapWithDelay(commands, endpoint, delay=200) { + delayBetween(commands.collect{ encap(it, endpoint) }, delay) +} diff --git a/devicetypes/smartthings/zwave-device.src/zwave-device.groovy b/devicetypes/smartthings/zwave-device.src/zwave-device.groovy new file mode 100644 index 00000000000..2c4958f7d4e --- /dev/null +++ b/devicetypes/smartthings/zwave-device.src/zwave-device.groovy @@ -0,0 +1,146 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Device", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Switch Level" + capability "Refresh" + capability "Sensor" + + fingerprint deviceId: "0x" + fingerprint deviceId: "0x3101" // for z-wave certification, can remove these when sub-meters/window-coverings are supported + fingerprint deviceId: "0x3101", inClusters: "0x86,0x32" + fingerprint deviceId: "0x09", inClusters: "0x86,0x72,0x26" + fingerprint deviceId: "0x0805", inClusters: "0x47,0x86,0x72" + } + + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.unknown.zwave.device", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.unknown.zwave.device", backgroundColor: "#ffffff" + } + standardTile("switchOn", "device.switch", inactiveLabel: false, decoration: "flat") { + state "on", label:'on', action:"switch.on", icon:"st.switches.switch.on" + } + standardTile("switchOff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "off", label:'off', action:"switch.off", icon:"st.switches.switch.off" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main "switch" + details (["switch", "switchOn", "switchOff", "levelSliderControl", "refresh"]) + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else { + def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + if (cmd) { + result += zwaveEvent(cmd) + } + } + return result +} + +def updated() { + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + [ createEvent(descriptionText: "${device.displayName} woke up"), + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) ] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + if (cmd.value == 0) { + createEvent(name: "switch", value: "off") + } else if (cmd.value == 255) { + createEvent(name: "switch", value: "on") + } else { + [ createEvent(name: "switch", value: "on"), createEvent(name: "switchLevel", value: cmd.value) ] + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + if (encapsulatedCommand) { + state.sec = 1 + def result = zwaveEvent(encapsulatedCommand) + result = result.collect { + if (it instanceof physicalgraph.device.HubAction && !it.toString().startsWith("9881")) { + response(cmd.CMD + "00" + it.toString()) + } else { + it + } + } + result + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + // def encapsulatedCommand = cmd.encapsulatedCommand(versions) + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", isStateChange: true) +} + +def on() { + commands([zwave.basicV1.basicSet(value: 0xFF), zwave.basicV1.basicGet()]) +} + +def off() { + commands([zwave.basicV1.basicSet(value: 0x00), zwave.basicV1.basicGet()]) +} + +def refresh() { + command(zwave.basicV1.basicGet()) +} + +def setLevel(value) { + commands([zwave.basicV1.basicSet(value: value as Integer), zwave.basicV1.basicGet()], 4000) +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} 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 new file mode 100644 index 00000000000..293be995c94 --- /dev/null +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy @@ -0,0 +1,256 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Z-Wave Door/Window Sensor + * + * Author: SmartThings + * Date: 2013-11-3 + */ + +metadata { + definition (name: "Z-Wave Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + + fingerprint deviceId: "0x2001", inClusters: "0x30,0x80,0x84,0x85,0x86,0x72" + fingerprint deviceId: "0x07", inClusters: "0x30" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82" + } + + // simulator metadata + simulator { + // status messages + status "open": "command: 2001, payload: FF" + status "closed": "command: 2001, payload: 00" + } + + // UI tile definitions + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "contact" + details(["contact", "battery"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err 106")) { + if (state.sec) { + log.debug description + } else { + result = createEvent( + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + isStateChange: true, + ) + } + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "parsed '$description' to $result" + return result +} + +def updated() { + def cmds = [] + if (!state.MSR) { + cmds = [ + zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), + "delay 1200", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ] + } else if (!state.lastbat) { + cmds = [] + } else { + cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + } + response(cmds) +} + +def configure() { + delayBetween([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), + batteryGetCommand() + ], 6000) +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x06 && cmd.event == 0x16) { + result << sensorValueEvent(1) + } else if (cmd.notificationType == 0x06 && cmd.event == 0x17) { + result << sensorValueEvent(0) + } else if (cmd.notificationType == 0x07) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice door/window sensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) + if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else if (cmd.event == 0x07) { + if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion") + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + if (!state.MSR) { + cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId).format() + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + cmds << "delay 1200" + } + if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { + cmds << batteryGetCommand() + } else { + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = now() + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + retypeBasedOnMSR() + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + if (msr == "011A-0601-0901") { // Enerwave motion doesn't always get the associationSet that the hub sends on join + result << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + } else if (!device.currentState("battery")) { + if (msr == "0086-0102-0059") { + result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) + } else { + result << response(batteryGetCommand()) + } + } + + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1]) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +def batteryGetCommand() { + def cmd = zwave.batteryV1.batteryGet() + if (state.sec) { + cmd = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd) + } + cmd.format() +} + +def retypeBasedOnMSR() { + switch (state.MSR) { + case "0086-0002-002D": + log.debug("Changing device type to Z-Wave Water Sensor") + setDeviceType("Z-Wave Water Sensor") + break + case "011F-0001-0001": // Schlage motion + case "014A-0001-0001": // Ecolink motion + case "0060-0001-0002": // Everspring SP814 + case "0060-0001-0003": // Everspring HSP02 + case "011A-0601-0901": // Enerwave ZWN-BPC + log.debug("Changing device type to Z-Wave Motion Sensor") + setDeviceType("Z-Wave Motion Sensor") + break + + } +} diff --git a/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy b/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy new file mode 100644 index 00000000000..f22faf812e9 --- /dev/null +++ b/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy @@ -0,0 +1,300 @@ +/** + * Z-Wave Garage Door Opener + * + * Copyright 2014 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Garage Door Opener", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Door Control" + capability "Garage Door Control" + capability "Contact Sensor" + capability "Refresh" + capability "Sensor" + + fingerprint deviceId: "0x4007", inClusters: "0x98" + fingerprint deviceId: "0x4006", inClusters: "0x98" + } + + simulator { + status "closed": "command: 9881, payload: 00 66 03 00" + status "opening": "command: 9881, payload: 00 66 03 FE" + status "open": "command: 9881, payload: 00 66 03 FF" + status "closing": "command: 9881, payload: 00 66 03 FC" + status "unknown": "command: 9881, payload: 00 66 03 FD" + + reply "988100660100": "command: 9881, payload: 00 66 03 FC" + reply "9881006601FF": "command: 9881, payload: 00 66 03 FE" + } + + tiles { + standardTile("toggle", "device.door", width: 2, height: 2) { + state("unknown", label:'${name}', action:"refresh.refresh", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening") + state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing") + state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e") + state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e") + + } + standardTile("open", "device.door", inactiveLabel: false, decoration: "flat") { + state "default", label:'open', action:"door control.open", icon:"st.doors.garage.garage-opening" + } + standardTile("close", "device.door", inactiveLabel: false, decoration: "flat") { + state "default", label:'close', action:"door control.close", icon:"st.doors.garage.garage-closing" + } + standardTile("refresh", "device.door", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "toggle" + details(["toggle", "open", "close", "refresh"]) + } +} + +import physicalgraph.zwave.commands.barrieroperatorv1.* + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "This device failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else { + def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2 ]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "\"$description\" parsed to ${result.inspect()}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1]) + log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() + if (cmd.commandClassControl) { + state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() + } + log.debug "Security command classes: $state.sec" + createEvent(name:"secureInclusion", value:"success", descriptionText:"$device.displayText is securely included") +} + +def zwaveEvent(BarrierOperatorReport cmd) { + def result = [] + def map = [ name: "door" ] + switch (cmd.barrierState) { + case BarrierOperatorReport.BARRIER_STATE_CLOSED: + map.value = "closed" + result << createEvent(name: "contact", value: "closed", displayed: false) + break + case BarrierOperatorReport.BARRIER_STATE_UNKNOWN_POSITION_MOVING_TO_CLOSE: + map.value = "closing" + break + case BarrierOperatorReport.BARRIER_STATE_UNKNOWN_POSITION_STOPPED: + map.descriptionText = "$device.displayName door state is unknown" + map.value = "unknown" + break + case BarrierOperatorReport.BARRIER_STATE_UNKNOWN_POSITION_MOVING_TO_OPEN: + map.value = "opening" + result << createEvent(name: "contact", value: "open", displayed: false) + break + case BarrierOperatorReport.BARRIER_STATE_OPEN: + map.value = "open" + result << createEvent(name: "contact", value: "open", displayed: false) + break + } + result + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + def map = [:] + if (cmd.notificationType == 6) { + map.displayed = true + switch(cmd.event) { + case 0x40: + if (cmd.eventParameter[0]) { + map.descriptionText = "$device.displayName performing initialization process" + } else { + map.descriptionText = "$device.displayName initialization process complete" + } + break + case 0x41: + map.descriptionText = "$device.displayName door operation force has been exceeded" + break + case 0x42: + map.descriptionText = "$device.displayName motor has exceeded operational time limit" + break + case 0x43: + map.descriptionText = "$device.displayName has exceeded physical mechanical limits" + break + case 0x44: + map.descriptionText = "$device.displayName unable to perform requested operation (UL requirement)" + break + case 0x45: + map.descriptionText = "$device.displayName remote operation disabled (UL requirement)" + break + case 0x46: + map.descriptionText = "$device.displayName failed to perform operation due to device malfunction" + break + case 0x47: + if (cmd.eventParameter[0]) { + map.descriptionText = "$device.displayName vacation mode enabled" + } else { + map.descriptionText = "$device.displayName vacation mode disabled" + } + break + case 0x48: + if (cmd.eventParameter[0]) { + map.descriptionText = "$device.displayName safety beam obstructed" + } else { + map.descriptionText = "$device.displayName safety beam obstruction cleared" + } + break + case 0x49: + if (cmd.eventParameter[0]) { + map.descriptionText = "$device.displayName door sensor ${cmd.eventParameter[0]} not detected" + } else { + map.descriptionText = "$device.displayName door sensor not detected" + } + break + case 0x4A: + if (cmd.eventParameter[0]) { + map.descriptionText = "$device.displayName door sensor ${cmd.eventParameter[0]} has a low battery" + } else { + map.descriptionText = "$device.displayName door sensor has a low battery" + } + result << createEvent(name: "battery", value: 1, unit: "%", descriptionText: map.descriptionText) + break + case 0x4B: + map.descriptionText = "$device.displayName detected a short in wall station wires" + break + case 0x4C: + map.descriptionText = "$device.displayName is associated with non-Z-Wave remote control" + break + default: + map.descriptionText = "$device.displayName: access control alarm $cmd.event" + map.displayed = false + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 1: + case 2: + map.descriptionText = "$device.displayName detected intrusion" + break + case 3: + map.descriptionText = "$device.displayName tampering detected: product cover removed" + break + case 4: + map.descriptionText = "$device.displayName tampering detected: incorrect code" + break + case 7: + case 8: + map.descriptionText = "$device.displayName detected motion" + break + default: + map.descriptionText = "$device.displayName: security alarm $cmd.event" + map.displayed = false + } + } else if (cmd.notificationType){ + map.descriptionText = "$device.displayName: alarm type $cmd.notificationType event $cmd.event" + } else { + map.descriptionText = "$device.displayName: alarm $cmd.v1AlarmType is ${cmd.v1AlarmLevel == 255 ? 'active' : cmd.v1AlarmLevel ?: 'inactive'}" + } + result ? [createEvent(map), *result] : createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery" + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + def msg = cmd.status == 0 ? "try again later" : + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def open() { + secure(zwave.barrierOperatorV1.barrierOperatorSet(requestedBarrierState: BarrierOperatorSet.REQUESTED_BARRIER_STATE_OPEN)) +} + +def close() { + secure(zwave.barrierOperatorV1.barrierOperatorSet(requestedBarrierState: BarrierOperatorSet.REQUESTED_BARRIER_STATE_CLOSE)) +} + +def refresh() { + secure(zwave.barrierOperatorV1.barrierOperatorGet()) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} diff --git a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy new file mode 100644 index 00000000000..8cbd8c98f0f --- /dev/null +++ b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy @@ -0,0 +1,686 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Lock", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Lock" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Lock Codes" + capability "Battery" + + command "unlockwtimeout" + + fingerprint deviceId: "0x4003", inClusters: "0x98" + fingerprint deviceId: "0x4004", inClusters: "0x98" + } + + simulator { + status "locked": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" + status "unlocked": "command: 9881, payload: 00 62 03 00 00 00 FE FE" + + reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" + reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 00 FE FE" + } + + tiles { + standardTile("toggle", "device.lock", width: 2, height: 2) { + state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking" + state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" + state "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" + state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" + state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + } + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking" + } + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "toggle" + details(["toggle", "lock", "unlock", "battery", "refresh"]) + } +} + +import physicalgraph.zwave.commands.doorlockv1.* +import physicalgraph.zwave.commands.usercodev1.* + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else { + def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "\"$description\" parsed to ${result.inspect()}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1]) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() + if (cmd.commandClassControl) { + state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() + } + log.debug "Security command classes: $state.sec" + createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included") +} + +def zwaveEvent(DoorLockOperationReport cmd) { + def result = [] + def map = [ name: "lock" ] + if (cmd.doorLockMode == 0xFF) { + map.value = "locked" + } else if (cmd.doorLockMode >= 0x40) { + map.value = "unknown" + } else if (cmd.doorLockMode & 1) { + map.value = "unlocked with timeout" + } else { + map.value = "unlocked" + if (state.assoc != zwaveHubNodeId) { + log.debug "setting association" + result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))) + result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)) + result << response(secure(zwave.associationV1.associationGet(groupingIdentifier:1))) + } + } + result ? [createEvent(map), *result] : createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { + def result = [] + def map = null + if (cmd.zwaveAlarmType == 6) { + if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) { + map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ] + } + switch(cmd.zwaveAlarmEvent) { + case 1: + map.descriptionText = "$device.displayName was manually locked" + break + case 2: + map.descriptionText = "$device.displayName was manually unlocked" + break + case 5: + if (cmd.eventParameter) { + map.descriptionText = "$device.displayName was locked with code ${cmd.eventParameter.first()}" + map.data = [ usedCode: cmd.eventParameter[0] ] + } + break + case 6: + if (cmd.eventParameter) { + map.descriptionText = "$device.displayName was unlocked with code ${cmd.eventParameter.first()}" + map.data = [ usedCode: cmd.eventParameter[0] ] + } + break + case 9: + map.descriptionText = "$device.displayName was autolocked" + break + case 7: + case 8: + case 0xA: + map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ] + break + case 0xB: + map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName is jammed" ] + break + case 0xC: + map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] + allCodesDeleted() + break + case 0xD: + if (cmd.eventParameter) { + map = [ name: "codeReport", value: cmd.eventParameter[0], data: [ code: "" ], isStateChange: true ] + map.descriptionText = "$device.displayName code ${map.value} was deleted" + map.isStateChange = (state["code$map.value"] != "") + state["code$map.value"] = "" + } else { + map = [ name: "codeChanged", descriptionText: "$device.displayName: user code deleted", isStateChange: true ] + } + break + case 0xE: + map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName: user code added", isStateChange: true ] + if (cmd.eventParameter) { + map.value = cmd.eventParameter[0] + result << response(requestCode(cmd.eventParameter[0])) + } + break + case 0xF: + map = [ name: "codeChanged", descriptionText: "$device.displayName: user code not added, duplicate", isStateChange: true ] + break + case 0x10: + map = [ name: "tamper", value: "detected", descriptionText: "$device.displayName: keypad temporarily disabled", displayed: true ] + break + case 0x11: + map = [ descriptionText: "$device.displayName: keypad is busy" ] + break + case 0x12: + map = [ name: "codeChanged", descriptionText: "$device.displayName: program code changed", isStateChange: true ] + break + case 0x13: + map = [ name: "tamper", value: "detected", descriptionText: "$device.displayName: code entry attempt limit exceeded", displayed: true ] + break + default: + map = map ?: [ descriptionText: "$device.displayName: alarm event $cmd.zwaveAlarmEvent", displayed: false ] + break + } + } else if (cmd.zwaveAlarmType == 7) { + map = [ name: "tamper", value: "detected", displayed: true ] + switch (cmd.zwaveAlarmEvent) { + case 0: + map.value = "clear" + map.descriptionText = "$device.displayName: tamper alert cleared" + break + case 1: + case 2: + map.descriptionText = "$device.displayName: intrusion attempt detected" + break + case 3: + map.descriptionText = "$device.displayName: covering removed" + break + case 4: + map.descriptionText = "$device.displayName: invalid code" + break + default: + map.descriptionText = "$device.displayName: tamper alarm $cmd.zwaveAlarmEvent" + break + } + } else switch(cmd.alarmType) { + case 21: // Manually locked + case 18: // Locked with keypad + case 24: // Locked by command (Kwikset 914) + case 27: // Autolocked + map = [ name: "lock", value: "locked" ] + break + case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked + case 19: + map = [ name: "lock", value: "unlocked" ] + if (cmd.alarmLevel) { + map.descriptionText = "$device.displayName was unlocked with code $cmd.alarmLevel" + map.data = [ usedCode: cmd.alarmLevel ] + } + break + case 22: + case 25: // Kwikset 914 unlocked by command + map = [ name: "lock", value: "unlocked" ] + break + case 9: + case 17: + case 23: + case 26: + map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName bolt is jammed" ] + break + case 13: + map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel was added", isStateChange: true ] + result << response(requestCode(cmd.alarmLevel)) + break + case 32: + map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] + allCodesDeleted() + case 33: + map = [ name: "codeReport", value: cmd.alarmLevel, data: [ code: "" ], isStateChange: true ] + map.descriptionText = "$device.displayName code $cmd.alarmLevel was deleted" + map.isStateChange = (state["code$cmd.alarmLevel"] != "") + state["code$cmd.alarmLevel"] = "" + break + case 112: + map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel changed", isStateChange: true ] + result << response(requestCode(cmd.alarmLevel)) + break + case 130: // Yale YRD batteries replaced + map = [ descriptionText: "$device.displayName batteries replaced", isStateChange: true ] + break + case 131: + map = [ /*name: "codeChanged", value: cmd.alarmLevel,*/ descriptionText: "$device.displayName code $cmd.alarmLevel is duplicate", isStateChange: false ] + break + case 161: + if (cmd.alarmLevel == 2) { + map = [ descriptionText: "$device.displayName front escutcheon removed", isStateChange: true ] + } else { + map = [ descriptionText: "$device.displayName detected failed user code attempt", isStateChange: true ] + } + break + case 167: + if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) { + map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ] + result << response(secure(zwave.batteryV1.batteryGet())) + } else { + map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "$device.displayName: battery low", displayed: true ] + } + break + case 168: + map = [ name: "battery", value: 1, descriptionText: "$device.displayName: battery level critical", displayed: true ] + break + case 169: + map = [ name: "battery", value: 0, descriptionText: "$device.displayName: battery too low to operate lock", isStateChange: true ] + break + default: + map = [ displayed: false, descriptionText: "$device.displayName: alarm event $cmd.alarmType level $cmd.alarmLevel" ] + break + } + result ? [createEvent(map), *result] : createEvent(map) +} + +def zwaveEvent(UserCodeReport cmd) { + def result = [] + def name = "code$cmd.userIdentifier" + def code = cmd.code + def map = [:] + if (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED || + (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user && code != "**********")) + { + if (code == "**********") { // Schlage locks send us this instead of the real code + state.blankcodes = true + code = state["set$name"] ?: decrypt(state[name]) ?: code + state.remove("set$name".toString()) + } + if (!code && cmd.userIdStatus == 1) { // Schlage touchscreen sends blank code to notify of a changed code + map = [ name: "codeChanged", value: cmd.userIdentifier, displayed: true, isStateChange: true ] + map.descriptionText = "$device.displayName code $cmd.userIdentifier " + (state[name] ? "changed" : "was added") + code = state["set$name"] ?: decrypt(state[name]) ?: "****" + state.remove("set$name".toString()) + } else { + map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: code ] ] + map.descriptionText = "$device.displayName code $cmd.userIdentifier is set" + map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) + map.isStateChange = (code != decrypt(state[name])) + } + result << createEvent(map) + } else { + map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: "" ] ] + if (state.blankcodes && state["reset$name"]) { // we deleted this code so we can tell that our new code gets set + map.descriptionText = "$device.displayName code $cmd.userIdentifier was reset" + map.displayed = map.isStateChange = false + result << createEvent(map) + state["set$name"] = state["reset$name"] + result << response(setCode(cmd.userIdentifier, state["reset$name"])) + state.remove("reset$name".toString()) + } else { + if (state[name]) { + map.descriptionText = "$device.displayName code $cmd.userIdentifier was deleted" + } else { + map.descriptionText = "$device.displayName code $cmd.userIdentifier is not set" + } + map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) + map.isStateChange = state[name] as Boolean + result << createEvent(map) + } + code = "" + } + state[name] = code ? encrypt(code) : code + + if (cmd.userIdentifier == state.requestCode) { // reloadCodes() was called, keep requesting the codes in order + if (state.requestCode + 1 > state.codes || state.requestCode >= 30) { + state.remove("requestCode") // done + } else { + state.requestCode = state.requestCode + 1 // get next + result << response(requestCode(state.requestCode)) + } + } + if (cmd.userIdentifier == state.pollCode) { + if (state.pollCode + 1 > state.codes || state.pollCode >= 30) { + state.remove("pollCode") // done + } else { + state.pollCode = state.pollCode + 1 + } + } + log.debug "code report parsed to ${result.inspect()}" + result +} + +def zwaveEvent(UsersNumberReport cmd) { + def result = [] + state.codes = cmd.supportedUsers + if (state.requestCode && state.requestCode <= cmd.supportedUsers) { + result << response(requestCode(state.requestCode)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + def result = [] + if (cmd.nodeId.any { it == zwaveHubNodeId }) { + state.remove("associationQuery") + log.debug "$device.displayName is associated to $zwaveHubNodeId" + result << createEvent(descriptionText: "$device.displayName is associated") + state.assoc = zwaveHubNodeId + if (cmd.groupingIdentifier == 2) { + result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + } + } else if (cmd.groupingIdentifier == 1) { + result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))) + } else if (cmd.groupingIdentifier == 2) { + result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet cmd) { + def result = [] + def now = new Date().toCalendar() + if(location.timeZone) now.timeZone = location.timeZone + result << createEvent(descriptionText: "$device.displayName requested time update", displayed: false) + result << response(secure(zwave.timeV1.timeReport( + hourLocalTime: now.get(Calendar.HOUR_OF_DAY), + minuteLocalTime: now.get(Calendar.MINUTE), + secondLocalTime: now.get(Calendar.SECOND))) + ) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + // The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1 + def result = [ createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") ] + result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + if (state.assoc != zwaveHubNodeId) { + result << response(zwave.associationV1.associationGet(groupingIdentifier:2)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery" + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + if (state.MSR == "003B-6341-5044") { + updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}") + } + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + def msg = cmd.status == 0 ? "try again later" : + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def lockAndCheck(doorLockMode) { + secureSequence([ + zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), + zwave.doorLockV1.doorLockOperationGet() + ], 4200) +} + +def lock() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED) +} + +def unlock() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED) +} + +def unlockwtimeout() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT) +} + +def refresh() { + def cmds = [secure(zwave.doorLockV1.doorLockOperationGet())] + if (state.assoc == zwaveHubNodeId) { + log.debug "$device.displayName is associated to ${state.assoc}" + } else if (!state.associationQuery) { + log.debug "checking association" + cmds << "delay 4200" + cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC + cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) + state.associationQuery = new Date().time + } else if (new Date().time - state.associationQuery.toLong() > 9000) { + log.debug "setting association" + cmds << "delay 6000" + cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() + cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() + cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) + state.associationQuery = new Date().time + } + log.debug "refresh sending ${cmds.inspect()}" + cmds +} + +def poll() { + def cmds = [] + if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) { + log.debug "setting association" + cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() + cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() + cmds << "delay 6000" + cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) + cmds << "delay 6000" + state.associationQuery = new Date().time + } else { + // Only check lock state if it changed recently or we haven't had an update in an hour + def latest = device.currentState("lock")?.date?.time + if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) { + cmds << secure(zwave.doorLockV1.doorLockOperationGet()) + state.lastPoll = (new Date()).time + } else if (!state.MSR) { + cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } else if (!state.fw) { + cmds << zwave.versionV1.versionGet().format() + } else if (!state.codes) { + state.pollCode = 1 + cmds << secure(zwave.userCodeV1.usersNumberGet()) + } else if (state.pollCode && state.pollCode <= state.codes) { + cmds << requestCode(state.pollCode) + } else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) { + cmds << secure(zwave.batteryV1.batteryGet()) + } else if (!state.enc) { + encryptCodes() + state.enc = 1 + } + } + log.debug "poll is sending ${cmds.inspect()}" + device.activity() + cmds ?: null +} + +private def encryptCodes() { + def keys = new ArrayList(state.keySet().findAll { it.startsWith("code") }) + keys.each { key -> + def match = (key =~ /^code(\d+)$/) + if (match) try { + def keynum = match[0][1].toInteger() + if (keynum > 30 && !state[key]) { + state.remove(key) + } else if (state[key] && !state[key].startsWith("~")) { + log.debug "encrypting $key: ${state[key].inspect()}" + state[key] = encrypt(state[key]) + } + } catch (java.lang.NumberFormatException e) { } + } +} + +def requestCode(codeNumber) { + secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeNumber)) +} + +def reloadAllCodes() { + def cmds = [] + if (!state.codes) { + state.requestCode = 1 + cmds << secure(zwave.userCodeV1.usersNumberGet()) + } else { + if(!state.requestCode) state.requestCode = 1 + cmds << requestCode(codeNumber) + } + cmds +} + +def setCode(codeNumber, code) { + def strcode = code + log.debug "setting code $codeNumber to $code" + if (code instanceof String) { + code = code.toList().findResults { if(it > ' ' && it != ',' && it != '-') it.toCharacter() as Short } + } else { + strcode = code.collect{ it as Character }.join() + } + if (state.blankcodes) { + // Can't just set, we won't be able to tell if it was successful + if (state["code$codeNumber"] != "") { + if (state["setcode$codeNumber"] != strcode) { + state["resetcode$codeNumber"] = strcode + return deleteCode(codeNumber) + } + } else { + state["setcode$codeNumber"] = strcode + } + } + secureSequence([ + zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:1, user:code), + zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) + ], 7000) +} + +def deleteCode(codeNumber) { + log.debug "deleting code $codeNumber" + secureSequence([ + zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:0), + zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) + ], 7000) +} + +def updateCodes(codeSettings) { + if(codeSettings instanceof String) codeSettings = util.parseJson(codeSettings) + def set_cmds = [] + def get_cmds = [] + codeSettings.each { name, updated -> + def current = decrypt(state[name]) + if (name.startsWith("code")) { + def n = name[4..-1].toInteger() + log.debug "$name was $current, set to $updated" + if (updated?.size() >= 4 && updated != current) { + def cmds = setCode(n, updated) + set_cmds << cmds.first() + get_cmds << cmds.last() + } else if ((current && updated == "") || updated == "0") { + def cmds = deleteCode(n) + set_cmds << cmds.first() + get_cmds << cmds.last() + } else if (updated && updated.size() < 4) { + // Entered code was too short + codeSettings["code$n"] = current + } + } else log.warn("unexpected entry $name: $updated") + } + if (set_cmds) { + return response(delayBetween(set_cmds, 2200) + ["delay 2200"] + delayBetween(get_cmds, 4200)) + } +} + +def getCode(codeNumber) { + decrypt(state["code$codeNumber"]) +} + +def getAllCodes() { + state.findAll { it.key.startsWith 'code' }.collectEntries { + [it.key, (it.value instanceof String && it.value.startsWith("~")) ? decrypt(it.value) : it.value] + } +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=4200) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (new Date().time - timestamp) > (seconds * 1000) +} + +private allCodesDeleted() { + if (state.codes instanceof Integer) { + (1..state.codes).each { n -> + if (state["code$n"]) { + result << createEvent(name: "codeReport", value: n, data: [ code: "" ], descriptionText: "code $n was deleted", + displayed: false, isStateChange: true) + } + state["code$n"] = "" + } + } +} diff --git a/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy b/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy new file mode 100644 index 00000000000..4631203385e --- /dev/null +++ b/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy @@ -0,0 +1,179 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Z-Wave Metering Dimmer + * + * Copyright 2014 SmartThings + * + */ +metadata { + definition (name: "Z-Wave Metering Dimmer", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Polling" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Switch Level" + capability "Sensor" + capability "Actuator" + + command "reset" + + fingerprint inClusters: "0x26,0x32" + } + + simulator { + status "on": "command: 2603, payload: FF" + status "off": "command: 2603, payload: 00" + status "09%": "command: 2603, payload: 09" + status "10%": "command: 2603, payload: 0A" + status "33%": "command: 2603, payload: 21" + status "66%": "command: 2603, payload: 42" + status "99%": "command: 2603, payload: 63" + + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + ["FF", "00", "09", "0A", "21", "42", "63"].each { val -> + reply "2001$val,delay 100,2602": "command: 2603, payload: $val" + } + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" + state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" + } + valueTile("power", "device.power") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + + main(["switch","power","energy"]) + details(["switch", "power", "energy", "levelSliderControl", "refresh", "reset"]) +} + +// parse events into attributes +def parse(String description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x32:3]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def updated() { + response(refresh()) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def dimmerEvents(physicalgraph.zwave.Command cmd) { + def result = [] + def value = (cmd.value ? "on" : "off") + def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") + result << switchEvent + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value, unit: "%") + } + if (switchEvent.isStateChange) { + result << response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()]) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + return createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + return createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + return createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } else { + return createEvent(name: "electric", value: cmd.scaledMeterValue, unit: ["pulses", "V", "A", "R/Z", ""][cmd.scale - 3]) + } + } +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format(), + ], 5000) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format(), + ], 5000) +} + +def poll() { + delayBetween([ + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format(), + ], 1000) +} + +def refresh() { + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format(), + ], 1000) +} + +def setLevel(level) { + if(level > 99) level = 99 + delayBetween([ + zwave.basicV1.basicSet(value: level).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 5000) +} diff --git a/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy b/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy new file mode 100644 index 00000000000..21ceb1bffce --- /dev/null +++ b/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy @@ -0,0 +1,198 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Metering Switch", namespace: "smartthings", author: "SmartThings") { + capability "Energy Meter" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Polling" + capability "Refresh" + capability "Configuration" + capability "Sensor" + + command "reset" + + fingerprint inClusters: "0x25,0x32" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("power", "device.power") { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy") { + state "default", label:'${currentValue} kWh' + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch","power","energy"]) + details(["switch","power","energy","refresh","reset"]) + } +} + +def updated() { + try { + if (!state.MSR) { + response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + } + } catch (e) { log.debug e } +} + +def parse(String description) { + def result = null + if(description == "updated") return + def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x72: 2]) + if (cmd) { + result = zwaveEvent(cmd) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { + if (cmd.scale == 0) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + def evt = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + if (evt.isStateChange) { + [evt, response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()])] + } else { + evt + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + // retypeBasedOnMSR() + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + + if (msr.startsWith("0086") && !state.aeonconfig) { // Aeon Labs meter + state.aeonconfig = 1 + result << response(delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // report power in watts + zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // report energy in kWh + zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report + //zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format(), // every 5 min + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format(), + ])) + } else { + result << response(delayBetween([ + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format(), + ])) + } + + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "$device.displayName: Unhandled: $cmd" + [:] +} + +def on() { + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def off() { + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.meterV2.meterGet(scale: 2).format() + ] +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.meterV2.meterGet(scale: 0).format(), + zwave.meterV2.meterGet(scale: 2).format() + ]) +} + +def configure() { + zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() +} + +def reset() { + return [ + zwave.meterV2.meterReset().format(), + zwave.meterV2.meterGet(scale: 0).format() + ] +} diff --git a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy new file mode 100644 index 00000000000..b46f9c64ed0 --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy @@ -0,0 +1,182 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Generic Z-Wave Motion Sensor + * + * Author: SmartThings + * Date: 2013-11-25 + */ + +metadata { + definition (name: "Z-Wave Motion Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Sensor" + capability "Battery" + } + + simulator { + status "inactive": "command: 3003, payload: 00" + status "active": "command: 3003, payload: FF" + } + + tiles { + standardTile("motion", "device.motion", width: 2, height: 2) { + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state("battery", label:'${currentValue}% battery', unit:"") + } + + main "motion" + details(["motion", "battery"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description) + } else { + def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } else { + result = createEvent(value: description, descriptionText: description, isStateChange: false) + } + } + return result +} + +def sensorValueEvent(Short value) { + if (value) { + createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else { + createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x07) { + if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) + if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else if (cmd.event == 0x07) { + if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + result << sensorValueEvent(1) + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) { + result << response(zwave.batteryV1.batteryGet()) + result << response("delay 1200") + } + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = new Date().time + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + break; + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = cmd.scale == 0 ? "%" : "" + break; + case 0x1E: + map.name = "loudness" + map.unit = cmd.scale == 1 ? "dBA" : "dB" + break; + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} diff --git a/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy b/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy new file mode 100644 index 00000000000..c68812fa899 --- /dev/null +++ b/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy @@ -0,0 +1,149 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + + definition (name: "Z-Wave Relay", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Relay Switch" + + fingerprint deviceId: "0x1001", inClusters: "0x20,0x25,0x27,0x72,0x86,0x70,0x85" + fingerprint deviceId: "0x1003", inClusters: "0x25,0x2B,0x2C,0x27,0x75,0x73,0x70,0x86,0x72" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","refresh"]) + } +} + +def installed() { + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + [name: "indicatorStatus", value: value, display: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + [name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + + final relays = [ + [manufacturerId:0x0113, productTypeId: 0x5246, productId: 0x3133, productName: "Evolve LFM-20"], + [manufacturerId:0x0113, productTypeId: 0x5246, productId: 0x3133, productName: "Linear FS20Z-1"], + [manufacturerId:0x5254, productTypeId: 0x8000, productId: 0x0002, productName: "Remotec ZFM-80"] + ] + + def productName = null + for (it in relays) { + if (it.manufacturerId == cmd.manufacturerId && it.productTypeId == cmd.productTypeId && it.productId == cmd.productId) { + productName = it.productName + break + } + } + + if (productName) { + log.debug "Relay found: $productName" + updateDataValue("productName", productName) + } + else { + log.debug "Not a relay, retyping to Z-Wave Switch" + setDeviceType("Z-Wave Switch") + } + [name: "manufacturer", value: cmd.manufacturerName] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def poll() { + zwave.switchBinaryV1.switchBinaryGet().format() +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ]) +} diff --git a/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy b/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy new file mode 100644 index 00000000000..bb1edb30486 --- /dev/null +++ b/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy @@ -0,0 +1,46 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Remote", namespace: "smartthings", author: "SmartThings") { + + fingerprint deviceId: "0x01" + } + + simulator { + + } + + tiles { + standardTile("state", "device.state", width: 2, height: 2) { + state "connected", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + + main "state" + details "state" + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} diff --git a/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy b/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy new file mode 100644 index 00000000000..16f991c5613 --- /dev/null +++ b/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy @@ -0,0 +1,223 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Sensor" + capability "Battery" + capability "Configuration" + + attribute "sensor", "string" + + fingerprint deviceId: "0xA1" + fingerprint deviceId: "0x21" + fingerprint deviceId: "0x20" + fingerprint deviceId: "0x07" + } + + simulator { + status "active": "command: 3003, payload: FF" + status "inactive": "command: 3003, payload: 00" + } + + tiles { + standardTile("sensor", "device.sensor", width: 2, height: 2) { + state("inactive", label:'inactive', icon:"st.unknown.zwave.sensor", backgroundColor:"#ffffff") + state("active", label:'active', icon:"st.unknown.zwave.sensor", backgroundColor:"#53a7c0") + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "sensor" + details(["sensor", "battery"]) + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x32: 3, 0x80: 1, 0x84: 1, 0x71: 1, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + return result +} + +def updated() { + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) +} + +def sensorValueEvent(Short value) { + if (value == 0) { + createEvent([ name: "sensor", value: "inactive" ]) + } else if (value == 255) { + createEvent([ name: "sensor", value: "active" ]) + } else { + [ createEvent([ name: "sensor", value: "active" ]), + createEvent([ name: "level", value: value ]) ] + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) +{ + sensorValueEvent(cmd.alarmLevel) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + break; + case 2: + map.name = "value" + map.unit = cmd.scale == 1 ? "%" : "" + break; + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + break; + case 4: + // power + map.name = "power" + map.unit = cmd.scale == 1 ? "Btu/h" : "W" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = cmd.scale == 0 ? "%" : "" + break; + case 6: + map.name = "velocity" + map.unit = cmd.scale == 1 ? "mph" : "m/s" + break; + case 8: + case 9: + map.name = "pressure" + map.unit = cmd.scale == 1 ? "inHg" : "kPa" + break; + case 0xE: + map.name = "weight" + map.unit = cmd.scale == 1 ? "lbs" : "kg" + break; + case 0xF: + map.name = "voltage" + map.unit = cmd.scale == 1 ? "mV" : "V" + break; + case 0x10: + map.name = "current" + map.unit = cmd.scale == 1 ? "mA" : "A" + break; + case 0x12: + map.name = "air flow" + map.unit = cmd.scale == 1 ? "cfm" : "m^3/h" + break; + case 0x1E: + map.name = "loudness" + map.unit = cmd.scale == 1 ? "dBA" : "dB" + break; + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [ displayed: true, value: cmd.scaledMeterValue ] + if (cmd.meterType == 1) { + map << ([ + [ name: "energy", unit: "kWh" ], + [ name: "energy", unit: "kVAh" ], + [ name: "power", unit: "W" ], + [ name: "pulse count", unit: "pulses" ], + [ name: "voltage", unit: "V" ], + [ name: "current", unit: "A"], + [ name: "power factor", unit: "R/Z"], + ][cmd.scale] ?: [ name: "electric" ]) + } else if (cmd.meterType == 2) { + map << [ name: "gas", unit: ["m^3", "ft^3", "", "pulses", ""][cmd.scale] ] + } else if (cmd.meterType == 3) { + map << [ name: "water", unit: ["m^3", "ft^3", "gal"][cmd.scale] ] + } else { + map << [ name: "meter", descriptionText: cmd.toString() ] + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def result = [] + result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + result << createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + result +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + } else { + map.value = cmd.batteryLevel + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) +{ + def versions = [0x20: 1, 0x30: 1, 0x31: 5, 0x32: 3, 0x80: 1, 0x84: 1, 0x71: 1, 0x9C: 1] + // def encapsulatedCommand = cmd.encapsulatedCommand(versions) + def version = versions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def event = [ displayed: false ] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: $cmd" + createEvent(event) +} + + +def configure() { + zwave.wakeUpV1.wakeUpNoMoreInformation().format() +} diff --git a/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy b/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy new file mode 100644 index 00000000000..d11466ee4c0 --- /dev/null +++ b/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy @@ -0,0 +1,159 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Everspring Siren + * + * Author: SmartThings + * Date: 2014-07-15 + */ +metadata { + definition (name: "Z-Wave Siren", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Alarm" + capability "Battery" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Switch" + + + fingerprint inClusters: "0x20,0x25,0x86,0x80,0x85,0x72,0x71" + } + + simulator { + // reply messages + reply "2001FF,2002": "command: 2003, payload: FF" + reply "200100,2002": "command: 2003, payload: 00" + reply "200121,2002": "command: 2003, payload: 21" + reply "200142,2002": "command: 2003, payload: 42" + reply "2001FF,delay 3000,200100,2002": "command: 2003, payload: 00" + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label:'off', action:'alarm.strobe', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + state "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "alarm" + details(["alarm","off","battery","refresh"]) + } +} + +def createEvents(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery" + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = new Date().time + createEvent(map) +} + +def poll() { + if (secondsPast(state.lastbatt, 36*60*60)) { + return zwave.batteryV1.batteryGet().format + } else { + return null + } +} + +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (new Date().time - timestamp) > (seconds * 1000) +} + +def on() { + log.debug "sending on" + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ] +} + +def off() { + log.debug "sending off" + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.basicV1.basicGet().format() + ] +} + +def strobe() { + log.debug "sending stobe/on command" + [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ] +} + +def refresh() { + log.debug "sending battery refresh command" + zwave.batteryV1.batteryGet().format() +} + +def parse(String description) { + log.debug "parse($description)" + def result = null + def cmd = zwave.parse(description, [0x20: 1]) + if (cmd) { + result = createEvents(cmd) + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + def switchValue = cmd.value ? "on" : "off" + def alarmValue + if (cmd.value == 0) { + alarmValue = "off" + } + else if (cmd.value <= 33) { + alarmValue = "strobe" + } + else if (cmd.value <= 66) { + alarmValue = "siren" + } + else { + alarmValue = "both" + } + [ + createEvent([name: "switch", value: switchValue, type: "digital", displayed: false]), + createEvent([name: "alarm", value: alarmValue, type: "digital"]) + ] +} + + +def createEvents(physicalgraph.zwave.Command cmd) { + log.warn "UNEXPECTED COMMAND: $cmd" +} diff --git a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy new file mode 100644 index 00000000000..168e6ec2a9c --- /dev/null +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -0,0 +1,179 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Smoke Alarm", namespace: "smartthings", author: "SmartThings") { + capability "Smoke Detector" + capability "Carbon Monoxide Detector" + capability "Sensor" + capability "Battery" + + attribute "alarmState", "string" + + fingerprint deviceId: "0xA100", inClusters: "0x20,0x80,0x70,0x85,0x71,0x72,0x86" + } + + simulator { + status "smoke": "command: 7105, payload: 01 FF" + status "clear": "command: 7105, payload: 01 00" + status "test": "command: 7105, payload: 0C FF" + status "carbonMonoxide": "command: 7105, payload: 02 FF" + status "carbonMonoxide clear": "command: 7105, payload: 02 00" + status "battery 100%": "command: 8003, payload: 64" + status "battery 5%": "command: 8003, payload: 05" + } + + tiles { + standardTile("smoke", "device.alarmState", width: 2, height: 2) { + state("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + state("smoke", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + state("carbonMonoxide", label:"MONOXIDE", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13") + state("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "smoke" + details(["smoke", "battery"]) + } +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results << createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + if (cmd) { + zwaveEvent(cmd, results) + } + } + // log.debug "\"$description\" parsed to ${results.inspect()}" + return results +} + + +def createSmokeOrCOEvents(name, results) { + def text = null + if (name == "smoke") { + text = "$device.displayName smoke was detected!" + // these are displayed:false because the composite event is the one we want to see in the app + results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false) + } else if (name == "carbonMonoxide") { + text = "$device.displayName carbon monoxide was detected!" + results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) + } else if (name == "tested") { + text = "$device.displayName was tested" + results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) + } else if (name == "smokeClear") { + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + name = "clear" + } else if (name == "carbonMonoxideClear") { + text = "$device.displayName carbon monoxide is clear" + results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) + name = "clear" + } else if (name == "testClear") { + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + name = "clear" + } + // This composite event is used for updating the tile + results << createEvent(name: "alarmState", value: name, descriptionText: text) +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { + if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_SMOKE) { + if (cmd.zwaveAlarmEvent == 3) { + createSmokeOrCOEvents("tested", results) + } else { + createSmokeOrCOEvents((cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "smoke" : "smokeClear", results) + } + } else if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_CO) { + createSmokeOrCOEvents((cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "carbonMonoxide" : "carbonMonoxideClear", results) + } else switch(cmd.alarmType) { + case 1: + createSmokeOrCOEvents(cmd.alarmLevel ? "smoke" : "smokeClear", results) + break + case 2: + createSmokeOrCOEvents(cmd.alarmLevel ? "carbonMonoxide" : "carbonMonoxideClear", results) + break + case 12: // test button pressed + createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results) + break + case 13: // sent every hour -- not sure what this means, just a wake up notification? + if (cmd.alarmLevel != 255) { + results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true) + } + + // Clear smoke in case they pulled batteries and we missed the clear msg + if(device.currentValue("smoke") != "clear") { + createSmokeOrCOEvents("smokeClear", results) + } + + // Check battery if we don't have a recent battery event + def prevBattery = device.currentState("battery") + if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) { + results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + } + break + default: + results << createEvent(displayed: true, descriptionText: "Alarm $cmd.alarmType ${cmd.alarmLevel == 255 ? 'activated' : cmd.alarmLevel ?: 'deactivated'}".toString()) + break + } +} + +// SensorBinary and SensorAlarm aren't tested, but included to preemptively support future smoke alarms +// +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd, results) { + if (cmd.sensorType == physicalgraph.zwave.commandclasses.SensorBinaryV2.SENSOR_TYPE_SMOKE) { + createSmokeOrCOEvents(cmd.sensorValue ? "smoke" : "smokeClear", results) + } else if (cmd.sensorType == physicalgraph.zwave.commandclasses.SensorBinaryV2.SENSOR_TYPE_CO) { + createSmokeOrCOEvents(cmd.sensorValue ? "carbonMonoxide" : "carbonMonoxideClear", results) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, results) { + if (cmd.sensorType == 1) { + createSmokeOrCOEvents(cmd.sensorState ? "smoke" : "smokeClear", results) + } else if (cmd.sensorType == 2) { + createSmokeOrCOEvents(cmd.sensorState ? "carbonMonoxide" : "carbonMonoxideClear", results) + } + +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { + results << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName battery is low!" + } else { + map.value = cmd.batteryLevel + } + results << createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, results) { + def event = [ displayed: false ] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: $cmd" + results << createEvent(event) +} diff --git a/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy b/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy new file mode 100644 index 00000000000..c35ebc0c0c9 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy @@ -0,0 +1,130 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Switch Secure", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Refresh" + capability "Polling" + capability "Actuator" + capability "Sensor" + + fingerprint inClusters: "0x25,0x98" + fingerprint deviceId: "0x10", inClusters: "0x98" + } + + simulator { + status "on": "command: 9881, payload: 002503FF" + status "off": "command: 9881, payload: 00250300" + + reply "9881002001FF,delay 200,9881002502": "command: 9881, payload: 002503FF" + reply "988100200100,delay 200,9881002502": "command: 9881, payload: 00250300" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","refresh"]) + } +} + +def updated() { + response(refresh()) +} + +def parse(description) { + def result = null + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent(descriptionText: description, isStateChange: true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + createEvent(name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled: $cmd" + null +} + +def on() { + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def off() { + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def poll() { + refresh() +} + +def refresh() { + command(zwave.switchBinaryV1.switchBinaryGet()) +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec != 0) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} diff --git a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy new file mode 100644 index 00000000000..74d1e140f55 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy @@ -0,0 +1,155 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Indicator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + + fingerprint inClusters: "0x25" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + standardTile("indicator", "device.indicatorStatus", inactiveLabel: false, decoration: "flat") { + state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" + state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" + state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","refresh","indicator"]) + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + [name: "indicatorStatus", value: value, display: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + [name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def poll() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ]) +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ]) +} + +def indicatorWhenOn() { + sendEvent(name: "indicatorStatus", value: "when on", display: false) + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() +} + +def indicatorWhenOff() { + sendEvent(name: "indicatorStatus", value: "when off", display: false) + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() +} + +def indicatorNever() { + sendEvent(name: "indicatorStatus", value: "never", display: false) + zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() +} + +def invertSwitch(invert=true) { + if (invert) { + zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 4, size: 1).format() + } + else { + zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format() + } +} diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy new file mode 100644 index 00000000000..404882ae99f --- /dev/null +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -0,0 +1,560 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Thermostat", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Thermostat" + capability "Configuration" + capability "Polling" + capability "Sensor" + + attribute "thermostatFanState", "string" + + command "switchMode" + command "switchFanMode" + command "quickSetCool" + command "quickSetHeat" + + fingerprint deviceId: "0x08" + fingerprint inClusters: "0x43,0x40,0x44,0x31" + } + + // simulator metadata + simulator { + status "off" : "command: 4003, payload: 00" + status "heat" : "command: 4003, payload: 01" + status "cool" : "command: 4003, payload: 02" + status "auto" : "command: 4003, payload: 03" + status "emergencyHeat" : "command: 4003, payload: 04" + + status "fanAuto" : "command: 4403, payload: 00" + status "fanOn" : "command: 4403, payload: 01" + status "fanCirculate" : "command: 4403, payload: 06" + + status "heat 60" : "command: 4303, payload: 01 09 3C" + status "heat 68" : "command: 4303, payload: 01 09 44" + status "heat 72" : "command: 4303, payload: 01 09 48" + + status "cool 72" : "command: 4303, payload: 02 09 48" + status "cool 76" : "command: 4303, payload: 02 09 4C" + status "cool 80" : "command: 4303, payload: 02 09 50" + + status "temp 58" : "command: 3105, payload: 01 2A 02 44" + status "temp 62" : "command: 3105, payload: 01 2A 02 6C" + status "temp 70" : "command: 3105, payload: 01 2A 02 BC" + status "temp 74" : "command: 3105, payload: 01 2A 02 E4" + status "temp 78" : "command: 3105, payload: 01 2A 03 0C" + status "temp 82" : "command: 3105, payload: 01 2A 03 34" + + status "idle" : "command: 4203, payload: 00" + status "heating" : "command: 4203, payload: 01" + status "cooling" : "command: 4203, payload: 02" + status "fan only" : "command: 4203, payload: 03" + status "pending heat" : "command: 4203, payload: 04" + status "pending cool" : "command: 4203, payload: 05" + status "vent economizer": "command: 4203, payload: 06" + + // reply messages + reply "2502": "command: 2503, payload: FF" + } + + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"switchMode", nextState:"to_heat" + state "heat", label:'${name}', action:"switchMode", nextState:"to_cool" + state "cool", label:'${name}', action:"switchMode", nextState:"..." + state "auto", label:'${name}', action:"switchMode", nextState:"..." + state "emergency heat", label:'${name}', action:"switchMode", nextState:"..." + state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool" + state "to_cool", label: "cool", action:"switchMode", nextState:"..." + state "...", label: "...", action:"off", nextState:"off" + } + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"switchFanMode" + state "fanOn", label:'${name}', action:"switchFanMode" + state "fanCirculate", label:'${name}', action:"switchFanMode" + } + controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff" + } + controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { + state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff" + } + standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + state "default", action:"polling.poll", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + main "temperature" + details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "configure"]) + } +} + +def parse(String description) +{ + def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]))) + if (!map) { + return null + } + + def result = [map] + if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) { + def map2 = [ + name: "thermostatSetpoint", + unit: getTemperatureScale() + ] + if (map.name == "thermostatMode") { + state.lastTriedMode = map.value + if (map.value == "cool") { + map2.value = device.latestValue("coolingSetpoint") + log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}" + } + else { + map2.value = device.latestValue("heatingSetpoint") + log.info "THERMOSTAT, latest heating setpoint = ${map2.value}" + } + } + else { + def mode = device.latestValue("thermostatMode") + log.info "THERMOSTAT, latest mode = ${mode}" + if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) { + map2.value = map.value + map2.unit = map.unit + } + } + if (map2.value != null) { + log.debug "THERMOSTAT, adding setpoint event: $map" + result << createEvent(map2) + } + } else if (map.name == "thermostatFanMode" && map.isStateChange) { + state.lastTriedFanMode = map.value + } + log.debug "Parse returned $result" + result +} + +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) +{ + def cmdScale = cmd.scale == 1 ? "F" : "C" + def map = [:] + map.value = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.displayed = false + switch (cmd.setpointType) { + case 1: + map.name = "heatingSetpoint" + break; + case 2: + map.name = "coolingSetpoint" + break; + default: + return [:] + } + // So we can respond with same format + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + map +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) +{ + def map = [:] + if (cmd.sensorType == 1) { + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + } else if (cmd.sensorType == 5) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) +{ + def map = [:] + switch (cmd.operatingState) { + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: + map.value = "idle" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING: + map.value = "heating" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING: + map.value = "cooling" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY: + map.value = "fan only" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT: + map.value = "pending heat" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL: + map.value = "pending cool" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER: + map.value = "vent economizer" + break + } + map.name = "thermostatOperatingState" + map +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { + def map = [name: "thermostatFanState", unit: ""] + switch (cmd.fanOperatingState) { + case 0: + map.value = "idle" + break + case 1: + map.value = "running" + break + case 2: + map.value = "running high" + break + } + map +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def map = [:] + switch (cmd.mode) { + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: + map.value = "off" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: + map.value = "heat" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: + map.value = "emergency heat" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: + map.value = "cool" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: + map.value = "auto" + break + } + map.name = "thermostatMode" + map +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { + def map = [:] + switch (cmd.fanMode) { + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: + map.value = "fanAuto" + break + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: + map.value = "fanOn" + break + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: + map.value = "fanCirculate" + break + } + map.name = "thermostatFanMode" + map.displayed = false + map +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def supportedModes = "" + if(cmd.off) { supportedModes += "off " } + if(cmd.heat) { supportedModes += "heat " } + if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " } + if(cmd.cool) { supportedModes += "cool " } + if(cmd.auto) { supportedModes += "auto " } + + state.supportedModes = supportedModes +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { + def supportedFanModes = "" + if(cmd.auto) { supportedFanModes += "fanAuto " } + if(cmd.low) { supportedFanModes += "fanOn " } + if(cmd.circulation) { supportedFanModes += "fanCirculate " } + + state.supportedFanModes = supportedFanModes +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "Zwave event received: $cmd" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unexpected zwave command $cmd" +} + +// Command Implementations +def poll() { + delayBetween([ + zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(), + zwave.thermostatModeV2.thermostatModeGet().format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format(), + zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format() + ], 2300) +} + +def quickSetHeat(degrees) { + setHeatingSetpoint(degrees, 1000) +} + +def setHeatingSetpoint(degrees, delay = 30000) { + setHeatingSetpoint(degrees.toDouble(), delay) +} + +def setHeatingSetpoint(Double degrees, Integer delay = 30000) { + log.trace "setHeatingSetpoint($degrees, $delay)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + + def convertedDegrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } else { + convertedDegrees = degrees + } + + delayBetween([ + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format() + ], delay) +} + +def quickSetCool(degrees) { + setCoolingSetpoint(degrees, 1000) +} + +def setCoolingSetpoint(degrees, delay = 30000) { + setCoolingSetpoint(degrees.toDouble(), delay) +} + +def setCoolingSetpoint(Double degrees, Integer delay = 30000) { + log.trace "setCoolingSetpoint($degrees, $delay)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + + def convertedDegrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } else { + convertedDegrees = degrees + } + + delayBetween([ + zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), + zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format() + ], delay) +} + +def configure() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSupportedGet().format(), + zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(), + zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format() + ], 2300) +} + +def modes() { + ["off", "heat", "cool", "auto", "emergency heat"] +} + +def switchMode() { + def currentMode = device.currentState("thermostatMode")?.value + def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" + def supportedModes = getDataByName("supportedModes") + def modeOrder = modes() + def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } + def nextMode = next(lastTriedMode) + if (supportedModes?.contains(currentMode)) { + while (!supportedModes.contains(nextMode) && nextMode != "off") { + nextMode = next(nextMode) + } + } + state.lastTriedMode = nextMode + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], 1000) +} + +def switchToMode(nextMode) { + def supportedModes = getDataByName("supportedModes") + if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" + if (nextMode in modes()) { + state.lastTriedMode = nextMode + "$nextMode"() + } else { + log.debug("no mode method '$nextMode'") + } +} + +def switchFanMode() { + def currentMode = device.currentState("thermostatFanMode")?.value + def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off" + def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn" + def modeOrder = ["fanAuto", "fanCirculate", "fanOn"] + def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } + def nextMode = next(lastTriedMode) + while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") { + nextMode = next(nextMode) + } + switchToFanMode(nextMode) +} + +def switchToFanMode(nextMode) { + def supportedFanModes = getDataByName("supportedFanModes") + if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" + + def returnCommand + if (nextMode == "fanAuto") { + returnCommand = fanAuto() + } else if (nextMode == "fanOn") { + returnCommand = fanOn() + } else if (nextMode == "fanCirculate") { + returnCommand = fanCirculate() + } else { + log.debug("no fan mode '$nextMode'") + } + if(returnCommand) state.lastTriedFanMode = nextMode + returnCommand +} + +def getDataByName(String name) { + state[name] ?: device.getDataValue(name) +} + +def getModeMap() { [ + "off": 0, + "heat": 1, + "cool": 2, + "auto": 3, + "emergency heat": 4 +]} + +def setThermostatMode(String value) { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def getFanModeMap() { [ + "auto": 0, + "on": 1, + "circulate": 6 +]} + +def setThermostatFanMode(String value) { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +def off() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def heat() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def emergencyHeat() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def cool() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def auto() { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], standardDelay) +} + +def fanOn() { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +def fanAuto() { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +def fanCirculate() { + delayBetween([ + zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(), + zwave.thermostatFanModeV3.thermostatFanModeGet().format() + ], standardDelay) +} + +private getStandardDelay() { + 1000 +} + diff --git a/devicetypes/smartthings/zwave-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy b/devicetypes/smartthings/zwave-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy new file mode 100644 index 00000000000..ac54c9af713 --- /dev/null +++ b/devicetypes/smartthings/zwave-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy @@ -0,0 +1,127 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartSense Virtual Momentary Contact Switch + * + * Author: SmartThings + * Date: 2013-03-07 + */ +metadata { + definition (name: "Z-Wave Virtual Momentary Contact Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Momentary" + capability "Sensor" + capability "Relay Switch" + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + // reply messages + reply "2001FF,2502,delay 2000,200100,2502": "command: 2503, payload: FF" + reply "200100,2502": "command: 2503, payload: 00" + } + + // tile definitions + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "momentary.push", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "switch" + details(["switch","refresh"]) + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, [0x20: 1]) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "physical"] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (state.manufacturer != cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + + final relays = [ + [manufacturerId:0x0113, productTypeId: 0x5246, productId: 0x3133, productName: "Evolve LFM-20"], + [manufacturerId:0x5254, productTypeId: 0x8000, productId: 0x0002, productName: "Remotec ZFM-80"] + ] + + def productName = null + for (it in relays) { + if (it.manufacturerId == cmd.manufacturerId && it.productTypeId == cmd.productTypeId && it.productId == cmd.productId) { + productName = it.productName + break + } + } + + if (productName) { + log.debug "Relay found: $productName" + updateDataValue("productName", productName) + } + [name: "manufacturer", value: cmd.manufacturerName] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def push() { + def cmds = [ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format(), + "delay 3000", + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ] +} + +def on() { + push() +} + +def off() { + [ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ] +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + ]) +} diff --git a/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy b/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy new file mode 100644 index 00000000000..7f500635f7f --- /dev/null +++ b/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy @@ -0,0 +1,177 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Generic Z-Wave Water Sensor + * + * Author: SmartThings + * Date: 2013-03-05 + */ + +metadata { + definition (name: "Z-Wave Water Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Water Sensor" + capability "Sensor" + capability "Battery" + + fingerprint deviceId: '0xA102', inClusters: '0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84,0x7A' + } + + simulator { + status "dry": "command: 3003, payload: 00" + status "wet": "command: 3003, payload: FF" + } + + tiles { + standardTile("water", "device.water", width: 2, height: 2) { + state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "water" + details(["water", "battery"]) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description) + } else { + def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } else { + result = createEvent(value: description, descriptionText: description, isStateChange: false) + } + } + return result +} + +def sensorValueEvent(Short value) { + def eventValue = value ? "wet" : "dry" + createEvent(name: "water", value: eventValue, descriptionText: "$device.displayName is $eventValue") +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) +{ + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) +{ + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) +{ + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x05) { + result << sensorValueEvent(cmd.event <= 2 ? 255 : 0) + } else if (cmd.notificationType == 0x04) { + if (cmd.event <= 0x02) { + result << createEvent(descriptionText: "$device.displayName detected overheat", isStateChange: true) + } else if (cmd.event <= 0x04) { + result << createEvent(descriptionText: "$device.displayName detected rapid temperature rise", isStateChange: true) + } else { + result << createEvent(descriptionText: "$device.displayName detected low temperature", isStateChange: true) + } + } else if (cmd.notificationType == 0x07) { + if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) +{ + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) { + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbat = new Date().time + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = cmd.scale == 0 ? "%" : "" + break; + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + if (msr == "0086-0002-002D") { // Aeon Water Sensor needs to have wakeup interval set + result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) + } + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) + result +} diff --git a/devicetypes/superuser/switch-too.src/switch-too.groovy b/devicetypes/superuser/switch-too.src/switch-too.groovy new file mode 100644 index 00000000000..03ac8297a92 --- /dev/null +++ b/devicetypes/superuser/switch-too.src/switch-too.groovy @@ -0,0 +1,48 @@ +/** + * Switch Too + * + * Copyright 2015 Bob Florian + * + * 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: "Switch Too", author: "Bob Florian") { + capability "Switch" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + // TODO: define your main and details tiles here + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + // TODO: handle 'switch' attribute + +} + +// handle commands +def on() { + log.debug "Executing 'on'" + // TODO: handle 'on' command +} + +def off() { + log.debug "Executing 'off'" + // TODO: handle 'off' command +} + + diff --git a/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy b/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy new file mode 100644 index 00000000000..d5ec4c0f408 --- /dev/null +++ b/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy @@ -0,0 +1,297 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Smart Block + * + * Author: steve + * Date: 2013-12-26 + */ + +metadata { + + definition (name: "Smart Block", namespace: "vlaminck/Minecraft", author: "SmartThings") { + capability "Switch Level" + capability "Switch" + + attribute "redstoneSignalStrength", "string" + attribute "smartBlockNeighborChanged", "string" + attribute "blockDestroyed", "string" + } + + simulator { + // TODO: define status and reply messages here + } + + preferences { + input title: "If you want SmartThings to update your SmartBlock, you must enter your Server's Address into your \"SmartBlock Manager\" SmartApp prior to placing the SmartBlock in Minecraft (Don't worry, it's safe to destroy a block and place it again).", type: "paragraph", element: "paragraph" +// input name: "serverIp", type: "text", title: "Server Address", description: "Where is your server located?", required: false + } + + tiles { + standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { + state "off", label: '${name}', icon: "st.switches.switch.off", backgroundColor: "#ffffff", action: "switch.on", nextState: "turningOn" + state "turningOn", label: '${name}', icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "on", label: '${name}', icon: "st.switches.switch.on", backgroundColor: "#79b821", action: "switch.off", nextState: "turningOff" + state "turningOff", label: '${name}', icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + valueTile("level", "device.level", height: 1, width: 1, inactiveLabel: false) { + state "level", label: '${currentValue}%', backgroundColor: "#ffffff" + } + valueTile("redstoneSignalStrength", "redstoneSignalStrength", inactiveLabel: false, decoration: "flat", width: 2) { + state "redstoneSignalStrength", label: 'Redstone Signal:\n${currentValue}', backgroundColor: "#ffffff" + } + standardTile("blockDestroyed", "device.blockDestroyed") { + state "false", label: 'OK', icon: "st.Health & Wellness.health9" + state "true", label: 'Destroyed', icon: "st.alarm.alarm.alarm" + } + valueTile("worldSeed", "device.worldSeed", decoration: "flat", width: 2) { + state "worldSeed", label: 'World Seed:\n${currentValue}' + } + valueTile("dimensionName", "device.dimensionName", decoration: "flat", width: 2) { + state "dimensionName", label: 'Dimension Name:\n${currentValue}' + } + valueTile("coordinates", "device.coordinates", decoration: "flat", width: 2) { + state "coordinates", label: 'Block Coordinates:\n${currentValue}' + } + valueTile("smartBlockNeighborChanged", "device.smartBlockNeighborChanged", decoration: "flat", width: 2) { + state "smartBlockNeighborChanged", label: 'Updated By:\n${currentValue}' + } + valueTile("blockServer", "device.blockServer", decoration: "flat", width: 2) { + state "blockServer", label: 'Server Address:\n${currentValue}' + } + valueTile("placedBy", "device.placedBy", decoration: "flat", width: 2) { + state "placedBy", label: 'Placed by:\n${currentValue}' + } + valueTile("closestPlayer", "device.closestPlayer", decoration: "flat", width: 2) { + state "closestPlayer", label: 'Last player close by:\n${currentValue}' + } + + main([ + "coordinates", + "switch", + "redstoneSignalStrength", + "level", + "smartBlockNeighborChanged", + "blockDestroyed" + ]) + details([ + "coordinates", + "switch", + "redstoneSignalStrength", + "level", + "smartBlockNeighborChanged", + "blockDestroyed", + "placedBy", + "closestPlayer", + "dimensionName", + "worldSeed", + "blockServer" + ]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" + // TODO: handle '' attribute + +} + + +def on() { + sendSwitchStateToMC("on") +} + +def off() { + sendSwitchStateToMC("off") +} + +def setLevel(newLevel) { + def signal = convertLevelToSignal(newLevel as int) + + sendSignalToMC(signal) +} + +def sendSignalToMC(newSignal) { + def url = "http://${state.serverIp}:3333/block?x=${state.x}&y=${state.y}&z=${state.z}&name=level&value=${newSignal}" + log.debug "POST to ${url}" + + httpPost(url, "foo=bar") { response -> + content = response.data + log.debug "response: ${content}" + } +} + +def sendSwitchStateToMC(switchState) { + def url = "http://${state.serverIp}:3333/block?x=${state.x}&y=${state.y}&z=${state.z}&name=switch&value=${switchState}" + log.debug "POST to ${url}" + + httpPost(url, "foo=bar") { response -> + content = response.data + log.debug "response: ${content}" + } +} + +def setCoordinates(x, y, z) { + state.x = x + state.y = y + state.z = z + log.debug "set block coordinates to: ${getCoordinates()}" + sendEvent(name: "coordinates", value: getCoordinates()) +} + +def getCoordinates() { + return "(${state?.x},${state?.y},${state?.z})" +} + +def getCoordinate(axis) { + if (!["x", "y", "z"].contains(axis)) + { + return null; + } + + return state."${axis}" +} + +def isDestroyed() { + state.destroyed +} + +def setDestroyed(isDestroyed) { + state.destroyed = isDestroyed + sendEvent(name: "blockDestroyed", value: isDestroyed) +} + +def setWorldSeed(worldSeed) { + state.worldSeed = worldSeed + sendEvent(name: "worldSeed", value: worldSeed) +} + +def setDimensionName(dimensionName) { + state.dimensionName = dimensionName + sendEvent(name: "dimensionName", value: dimensionName) +} + +def setPlacedBy(placedBy) { + state.placedBy = placedBy + sendEvent(name: "placedBy", value: placedBy) +} + +def setClosestPlayer(closestPlayer) { + state.closestPlayer = closestPlayer + state.closestPlayerChanged = new Date() + sendEvent(name: "closestPlayer", value: closestPlayer, isStateChange: true) +} + +def setSignalStrength(int signalStrength) { + state.signalStrength = signalStrength + + sendEvent(name: "switch", value: signalStrength > 0 ? "on" : "off") + sendEvent(name: "redstoneSignalStrength", value: signalStrength) + sendEvent(name: "level", value: convertSignalToLevel(signalStrength)) +} + +def setLastNeighborChanged(blockId, blockName) { + state.neighborBlockId = blockId + state.neighborBlockName = blockName + sendEvent(name: "smartBlockNeighborChanged", value: "${blockId} ${blockName}", isStateChange: true) +} + +def setServerIp(ip) { + + if (!ip) + { + return + } + + ip = ip.replace("https://", "") + ip = ip.replace("http://", "") + + def serverParts = ip.split(":") + if (serverParts.size() > 1) + { + state.serverPort = serverParts[1] + } + + if (serverParts.size() > 0) + { + state.serverIp = serverParts[0] + } + + if (state.serverIp) + { + state.blockServer = state.serverIp + } + if (state.serverPort) + { + state.blockServer = "${state.blockServer}:${state.serverPort}" + } + + sendEvent(name: "blockServer", value: "${state.blockServer}") + +} + + +def calculateDNI() { // not currently used, and probably not necessary + "${state.worldSeed}|${state.dimensionName}|${getCoordinates()}".encodeAsMD5() +} + +def neighborBlockChange(data) { +// data:{signalStrength:0,blockId:55,blockName:Redstone Dust,z:321,y:63,x:-135,worldSeed:,dimensionName:Overworld} + + log.debug "neighborBlockChange($data)" + + if (data?.closestPlayer) + { + setClosestPlayer(data?.closestPlayer) + } + + int signalStrength = data?.signalStrength as int + setSignalStrength(signalStrength) + + setLastNeighborChanged(data?.blockId, data?.blockName) + + if (data?.worldSeed) + { + setWorldSeed(data?.worldSeed) + } + + if (data?.dimensionName) + { + setDimensionName(data?.dimensionName) + } + + if (data?.placedBy) + { + setPlacedBy(data?.placedBy) + } + + sendEvent(name: "coordinates", value: getCoordinates()) +} + +def convertLevelToSignal(int level = 0) { + if (level <= 0) return 0 + if (level >= 99) return 15 + + int signal = (15 - ((99 - level) / 7)) + log.debug "converted level to signalStrength: $signal" + return signal +} + +def convertSignalToLevel(int signal = 0) { + if (signal <= 0) return 0 + if (signal >= 15) return 99 + + int level = (99 - ((15 - signal) * 7)) + log.debug "converted level to signalStrength: $signal" + return level +} diff --git a/devicetypes/wackford/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy b/devicetypes/wackford/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy new file mode 100644 index 00000000000..9e8ea7fb041 --- /dev/null +++ b/devicetypes/wackford/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy @@ -0,0 +1,106 @@ +/** + * Quirky-Wink-Eggtray-Device + * + * Author: todd@wackford.net + * Date: 2014-02-22 + * + ***************************************************************** + * Setup Namespace, acpabilities, attributes and commands + ***************************************************************** + * Namespace: "wackford" + * + * Capabilities: "polling" + * "refresh" + * + * Custom Attributes: "inventory" + * "totalEggs" + * "freshEggs" + * "oldEggs" + * "eggReport" + * + * Custom Commands: "eggReport" + * + ***************************************************************** + * Changes + ***************************************************************** + * Change 1: 2014-02-26 + * Added egg report + * implemented icons/tiles (thanks to Dane) + * + * Change 2: 2014-03-10 + * Documented Header + * + ***************************************************************** + * Code + ***************************************************************** + */ +// for the UI +metadata { + + definition(name:"Quirky Wink Eggtray", namespace:"wackford", author:"Todd Wackford") { + + capability "Polling" + capability "Refresh" + capability "Sensor" + + attribute "inventory", "enum", ["goodEggs","haveBadEgg","noEggs"] + attribute "totalEggs", "number" + attribute "freshEggs", "number" + attribute "oldEggs", "number" + + command "eggReport" + } + + tiles { + standardTile("inventory", "device.inventory", width: 2, height: 2){ + state "goodEggs", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#53a7c0" + state "haveBadEgg", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#FF1919" + state "noEggs", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#ffffff" + } + standardTile("totalEggs", "device.totalEggs", inactiveLabel: false){ + state "totalEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-count", backgroundColor: "#53a7c0" + } + standardTile("freshEggs", "device.freshEggs", inactiveLabel: false){ + state "freshEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-fresh", backgroundColor: "#53a7c0" + } + standardTile("oldEggs", "device.oldEggs", inactiveLabel: false){ + state "oldEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-expired", backgroundColor: "#53a7c0" + } + standardTile("eggReport", "device.eggReport", inactiveLabel: false, decoration: "flat"){ + state "eggReport", action: "eggReport", label : ' ', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-report" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + + main(["inventory", "totalEggs", "freshEggs", "oldEggs"]) + details(["inventory", "eggReport", "refresh", "totalEggs", "freshEggs", "oldEggs"]) + +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + if (description?.name && description?.value) + { + results << sendEvent(name: "${description?.name}", value: "${description?.value}") + } +} + +def eggReport() { + log.debug "Executing Egg Report" + parent.runEggReport(this) +} + +def poll() { + log.debug "Executing 'poll'" + parent.poll(this) +} + +def refresh() { + log.debug "Executing 'refresh'" + parent.poll(this) +} diff --git a/devicetypes/wackford/quirky-wink-nimbus.src/quirky-wink-nimbus.groovy b/devicetypes/wackford/quirky-wink-nimbus.src/quirky-wink-nimbus.groovy new file mode 100644 index 00000000000..dcfd1640465 --- /dev/null +++ b/devicetypes/wackford/quirky-wink-nimbus.src/quirky-wink-nimbus.groovy @@ -0,0 +1,79 @@ +/* + * Quirky-Wink-Nimbus-Device.groovy + * + * Author: todd@wackford.net + * Date: 2014-02-22 + * + ***************************************************************** + * Setup Namespace, acpabilities, attributes and commands + ***************************************************************** + * Namespace: "wackford" + * + * Capabilities: "polling" + * "refresh" + * + * Custom Attributes: "dial" + * "info" + * + * Custom Commands: "none" + * + ***************************************************************** + * Changes + ***************************************************************** + * + * Change 1: 2014-03-10 + * Documented Header + * + ***************************************************************** + * Code + ***************************************************************** + */ +// for the UI +metadata { + + definition(name:"Quirky Wink Nimbus", namespace:"wackford", author:"Todd Wackford") { + + capability "Polling" + capability "Refresh" + capability "Sensor" + + attribute "dial", "string" + attribute "info", "string" + } + + tiles { + standardTile("dial", "device.dial", width: 2, height: 2){ + state("dial", label : '${currentValue}', unit : "", icon:"st.custom.quirky.quirky-device" ) + } + valueTile("info", "device.info", inactiveLabel: false, decoration: "flat") { + state "info", label:'Dial is displaying ${currentValue}', unit:"" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + main(["dial"]) + details(["dial","info","refresh" ]) +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + if (description?.name && description?.value) + { + results << sendEvent(name: "${description?.name}", value: "${description?.value}") + } +} + + +def poll() { + log.debug "Nimbus executing 'pollNimbus'" + parent.pollNimbus(this) +} + +def refresh() { + log.debug "Nimbus executing 'refresh'" + parent.pollNimbus(this) +} diff --git a/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy b/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy new file mode 100644 index 00000000000..8bf827b780f --- /dev/null +++ b/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy @@ -0,0 +1,84 @@ +/* Quirky-Wink-Porkfolio-Device.groovy + * + * Author: todd@wackford.net + * Date: 2014-02-22 + * + ***************************************************************** + * Setup Namespace, acpabilities, attributes and commands + ***************************************************************** + * Namespace: "wackford" + * + * Capabilities: "acceleration" + * "battery" + * "polling" + * "refresh" + * + * Custom Attributes: "balance" + * "goal" + * + * Custom Commands: "none" + * + ***************************************************************** + * Changes + ***************************************************************** + * + * Change 1: 2014-03-10 + * Documented Header + * + ***************************************************************** + * Code + ***************************************************************** + */ +metadata { + + definition(name:"Quirky Wink Porkfolio", namespace:"wackford", author:"Todd Wackford") { + + capability "Acceleration Sensor" + capability "Battery" + capability "Polling" + capability "Refresh" + capability "Sensor" + + attribute "balance", "string" + attribute "goal", "string" + } + + tiles { + standardTile("acceleration", "device.acceleration", width: 2, height: 2, canChangeIcon: true) { + state "inactive", label:'pig secure', icon:"st.motion.acceleration.inactive", backgroundColor:"#44b621" + state "active", label:'pig alarm', icon:"st.motion.acceleration.active", backgroundColor:"#FF1919" + } + standardTile("balance", "device.balance", inactiveLabel: false, canChangeIcon: true) { + state "balance", label:'${currentValue}', unit:"", icon:"st.Food & Dining.dining18" + } + standardTile("goal", "device.goal", inactiveLabel: false, decoration: "flat", canChangeIcon: true) { + state "goal", label:'${currentValue} goal', unit:"", icon:"st.Weather.weather2" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + main(["acceleration", "balance"]) + details(["acceleration", "balance", "goal", "refresh" ]) +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + if (description?.name && description?.value) + { + results << sendEvent(name: "${description?.name}", value: "${description?.value}") + } +} + +def poll() { + log.debug "Executing 'poll'" + parent.poll(this) +} + +def refresh() { + log.debug "Executing 'refresh'" + parent.poll(this) +} diff --git a/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy b/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy new file mode 100644 index 00000000000..8dd6c27f8e5 --- /dev/null +++ b/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy @@ -0,0 +1,92 @@ +/* Quirky-Wink-Powerstrip-Device.groovy + * + * Author: todd@wackford.net + * Date: 2014-01-28 + * + ***************************************************************** + * Setup Namespace, acpabilities, attributes and commands + ***************************************************************** + * Namespace: "wackford" + * + * Capabilities: "switch" + * "polling" + * "refresh" + * + * Custom Attributes: "none" + * + * Custom Commands: "none" + * + ***************************************************************** + * Changes + ***************************************************************** + * + * Change 1: 2014-03-10 + * Documented Header + * + ***************************************************************** + * Code + ***************************************************************** + */ +// for the UI +metadata { + + definition(name:"Quirky Wink Powerstrip", namespace:"wackford", author:"Todd Wackford") { + + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Actuator" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + main(["switch"]) + details(["switch", "refresh" ]) +} + + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + if (description?.name && description?.value) + { + results << sendEvent(name: "${description?.name}", value: "${description?.value}") + } +} + + +// handle commands +def on() { + log.debug "Executing 'on'" + log.debug this + parent.on(this) +} + +def off() { + log.debug "Executing 'off'" + parent.off(this) +} + +def poll() { + log.debug "Executing 'poll'" + parent.pollOutlet(this) +} + +def refresh() { + log.debug "Executing 'refresh'" + poll() +} diff --git a/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy b/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy new file mode 100644 index 00000000000..8de12f2cd41 --- /dev/null +++ b/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy @@ -0,0 +1,120 @@ +/** + * Quirky-Wink-Spotter-Device.groovy + * + * Author: todd@wackford.net + * Date: 2014-02-19 + * + ***************************************************************** + * Setup Namespace, capabilities, attributes and commands + ***************************************************************** + * Namespace: "wackford" + * + * Capabilities: "Polling" + * "Battery" + * "Temperature Measurement" + * "Acceleration Sensor" + * "Refresh" + * "Motion Sensor" + * "Relative Humidity Measurement" + * + * Custom Attributes: "sound" + * "light" + * "powerSource" + * + * Custom Commands: "none" + * + ***************************************************************** + * Changes + ***************************************************************** + * + * Change 1: 2014-03-10 + * Documented Header + * + ***************************************************************** + * Code + ***************************************************************** + */ +// for the UI +metadata { + definition(name:"Quirky Wink Spotter", namespace:"wackford", author:"Todd Wackford") { + + capability "Polling" + capability "Battery" + capability "Temperature Measurement" + capability "Acceleration Sensor" + capability "Refresh" + capability "Motion Sensor" + capability "Relative Humidity Measurement" + capability "Sensor" + + attribute "sound", "enum", ["active","inactive"] + attribute "light", "enum", ["active","inactive"] + attribute "powerSource", "enum", ["powered","battery"] + } + + tiles { + standardTile("acceleration", "device.acceleration", width: 2, height: 2, canChangeIcon: true) { + state "active", label:'active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0" + state "inactive", label:'inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff" + } + valueTile("temperature", "device.temperature", canChangeIcon: false) + { + state("temperature", label : '${currentValue}°', unit : "F", + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } + valueTile("humidity", "device.humidity", inactiveLabel: false, canChangeIcon: false) { + state "humidity", label:'${currentValue}% RH', unit:"" + } + standardTile("sound", "device.sound", inactiveLabel: false) { + state "active", label: "noise", unit:"", icon: "st.alarm.beep.beep", backgroundColor: "#53a7c0" + state "inactive", label: "quiet", unit:"", icon: "st.alarm.beep.beep", backgroundColor: "#ffffff" + } + standardTile("light", "device.light", inactiveLabel: false, canChangeIcon: true) { + state "active", label: "light", unit:"", icon: "st.illuminance.illuminance.bright", backgroundColor: "#53a7c0" + state "inactive", label: "dark", unit:"", icon: "st.illuminance.illuminance.dark", backgroundColor: "#B2B2B2" + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, canChangeIcon: false) { + state "battery", label: '${currentValue}% battery' + } + standardTile("powerSource", "device.powerSource", inactiveLabel: false, canChangeIcon: true) { + state "powered", label: "powered", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "battery", label: "battery", icon: "st.switches.switch.on", backgroundColor: "#ffa81e" + } + standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + main(["acceleration", "temperature", "humidity", "sound", "light", "powerSource"]) + details(["acceleration", "temperature", "humidity", "sound", "light", "powerSource", "battery", "refresh" ]) +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + if (description?.name && description?.value) + { + results << sendEvent(name: "${description?.name}", value: "${description?.value}") + } +} + + +def poll() { + log.debug "Executing 'poll'" + parent.poll(this) +} + +def refresh() { + log.debug "Executing 'refresh'" + parent.poll(this) +} diff --git a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy new file mode 100644 index 00000000000..186c8b23431 --- /dev/null +++ b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy @@ -0,0 +1,228 @@ +/** + * TCP Bulb.groovy + * + * Author: todd@wackford.net + * Date: 2014-03-07 + * + * + ***************************************************************** + * Changes + ***************************************************************** + * + * Change 1: 2014-03-10 + * Documented Header + * + * Change 2: 2014-03-15 + * Fixed bug where we weren't coming on when changing + * levels down. + * + * Change 3: 2014-04-02 (lieberman) + * Changed sendEvent() to createEvent() in parse() + * + * Change 4: 2014-04-12 (wackford) + * Added current power usage tile + * + * Change 5: 2014-09-14 (wackford) + * a. Changed createEvent() to sendEvent() in parse() to + * fix tile not updating. + * b. Call IP checker for DHCP environments from refresh. Parent + * service manager has method to call every 5 minutes too. + * + * Change 6: 2014-10-17 (wackford) + * a. added step size input to settings of device + * b. added refresh on udate + * c. added uninstallFromChildDevice to handle removing from settings + * d. Changed to allow bulb to 100%, was possible to get past logic at 99 + * + * Change 7: 2014-11-09 (wackford) + * a. Added bulbpower calcs to device. TCP is broken + * b. Changed to set dim level first then on. Much easier on the eys coming from bright. + * + ***************************************************************** + * Code + ***************************************************************** + */ +// for the UI +metadata { + definition (name: "TCP Bulb", namespace: "wackford", author: "Todd Wackford") { + capability "Switch" + capability "Polling" + capability "Power Meter" + capability "Refresh" + capability "Switch Level" + + attribute "stepsize", "string" + + command "levelUp" + command "levelDown" + command "on" + command "off" + command "setBulbPower" + } + + simulator { + // TODO: define status and reply messages here + } + + preferences { + input "stepsize", "number", title: "Step Size", description: "Dimmer Step Size", defaultValue: 5 + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { + state "level", label: 'Level ${currentValue}%' + } + standardTile("lUp", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) { + state "default", action:"levelUp", icon:"st.illuminance.illuminance.bright" + } + standardTile("lDown", "device.switchLevel", inactiveLabel: false,decoration: "flat", canChangeIcon: false) { + state "default", action:"levelDown", icon:"st.illuminance.illuminance.light" + } + valueTile( "power", "device.power", inactiveLabel: false, decoration: "flat") { + state "power", label: '${currentValue} Watts' + } + + main(["switch"]) + details(["switch", "lUp", "lDown", "levelSliderControl", "level" , "power", "refresh" ]) + } +} + +// parse events into attributes +def parse(description) { + //log.debug "parse() - $description" + def results = [] + + if ( description == "updated" ) + return + + if (description?.name && description?.value) + { + results << createEvent(name: "${description?.name}", value: "${description?.value}") + } +} + +// handle commands +def setBulbPower(value) { + state.bulbPower = value + log.debug "In child with bulbPower of ${state.bulbPower}" +} + +def on() { + log.debug "Executing 'on'" + sendEvent(name:"switch",value:on) + parent.on(this) + + def levelSetting = device.latestValue("level") as Float ?: 1.0 + def bulbPowerMax = device.latestValue("setBulbPower") as Float + def calculatedPower = bulbPowerMax * (levelSetting / 100) + sendEvent(name: "power", value: calculatedPower.round(1)) + + if (device.latestValue("level") == null) { + sendEvent( name: "level", value: 1.0 ) + } +} + +def off() { + log.debug "Executing 'off'" + sendEvent(name:"switch",value:off) + parent.off(this) + sendEvent(name: "power", value: 0.0) +} + +def levelUp() { + def level = device.latestValue("level") as Integer ?: 0 + def step = state.stepsize as float + + level+= step + + if ( level > 100 ) + level = 100 + + setLevel(level) +} + +def levelDown() { + def level = device.latestValue("level") as Integer ?: 0 + def step = state.stepsize as float + + level-= step + + if ( level < 1 ) + level = 1 + + setLevel(level) +} + +def setLevel(value) { + log.debug "in setLevel with value: ${value}" + def level = value as Integer + + sendEvent( name: "level", value: level ) + sendEvent( name: "switch.setLevel", value:level ) + parent.setLevel( this, level ) + + + if (( level > 0 ) && ( level <= 100 )) + on() + else + off() + + def levelSetting = level as float + def bulbPowerMax = device.latestValue("setBulbPower") as float + def calculatedPower = bulbPowerMax * (levelSetting / 100) + sendEvent(name: "power", value: calculatedPower.round(1)) +} + +def poll() { + log.debug "Executing poll()" + parent.poll(this) +} + +def refresh() { + log.debug "Executing refresh()" + parent.poll(this) +} + +def installed() { + initialize() +} + +def updated() { + initialize() + refresh() +} + +def initialize() { + if ( !settings.stepsize ) + state.stepsize = 10 //set the default stepsize + else + state.stepsize = settings.stepsize +} + +/******************************************************************************* + Method :uninstalled(args) + (args) :none + returns:Nothing + ERRORS :No error handling is done + + Purpose:This is standard ST method. + Gets called when "remove" is selected in child device "preferences" + tile. It also get's called when "deleteChildDevice(child)" is + called from parent service manager app. + *******************************************************************************/ +def uninstalled() { + log.debug "Executing 'uninstall' in device type" + parent.uninstallFromChildDevice(this) +} diff --git a/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy new file mode 100644 index 00000000000..662bbc32726 --- /dev/null +++ b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy @@ -0,0 +1,541 @@ +/** + * Zen Thermostat + * + * Author: Zen Within + * Date: 2015-02-21 + */ +metadata { + definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") { + capability "Actuator" + capability "Thermostat" + capability "Configuration" + capability "Refresh" + capability "Sensor" + + fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019" + + //attribute "temperatureUnit", "number" + + command "setpointUp" + command "setpointDown" + + command "setCelsius" + command "setFahrenheit" + + // To please some of the thermostat SmartApps + command "poll" + } + + // simulator metadata + simulator { } + + tiles { + valueTile("frontTile", "device.temperature", width: 1, height: 1) { + state("temperature", label:'${currentValue}°', backgroundColor:"#e8e3d8") + } + + valueTile("temperature", "device.temperature", width: 1, height: 1) { + state("temperature", label:'${currentValue}°', backgroundColor:"#0A1E2C") + } + + standardTile("fanMode", "device.thermostatFanMode", decoration: "flat") { + state "fanAuto", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-auto" + state "fanOn", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-on" + } + + + standardTile("mode", "device.thermostatMode", decoration: "flat") { + state "off", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.heating-cooling-off", nextState:"heating" + state "heat", action:"setThermostatMode", backgroundColor:"#ff6e7e", icon:"st.thermostat.heat", nextState:"cooling" + state "cool", action:"setThermostatMode", backgroundColor:"#90d0e8", icon:"st.thermostat.cool", nextState:"..." + //state "auto", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.auto" + state "heating", action:"setThermostatMode", nextState:"to_cool" + state "cooling", action:"setThermostatMode", nextState:"..." + state "...", action:"off", nextState:"off" + } + + valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 2, height: 2) { + state "off", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" + state "heat", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" + state "cool", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false) { + state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false) { + state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("thermostatOperatingState", "device.thermostatOperatingState", inactiveLabel: false) { + state "heating", backgroundColor:"#ff6e7e" + state "cooling", backgroundColor:"#90d0e8" + state "fan only", backgroundColor:"#e8e3d8" + } + standardTile("setpointUp", "device.thermostatSetpoint", decoration: "flat") { + state "setpointUp", action:"setpointUp", icon:"st.thermostat.thermostat-up" + } + + standardTile("setpointDown", "device.thermostatSetpoint", decoration: "flat") { + state "setpointDown", action:"setpointDown", icon:"st.thermostat.thermostat-down" + } + + standardTile("refresh", "device.temperature", decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + standardTile("configure", "device.configure", decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main "frontTile" + details(["temperature", "fanMode", "mode", "thermostatSetpoint", "setpointUp", "setpointDown","refresh", "configure"]) + } +} + + +// parse events into attributes +def parse(String description) { + log.debug "Parse description $description" + def map = [:] + def activeSetpoint = "--" + + if (description?.startsWith("read attr -")) + { + def descMap = parseDescriptionAsMap(description) + // Thermostat Cluster Attribute Read Response + if (descMap.cluster == "0201" && descMap.attrId == "0000") + { + log.debug "LOCAL TEMPERATURE" + map.name = "temperature" + map.value = getTemperature(descMap.value) + def receivedTemperature = map.value + } + else if (descMap.cluster == "0201" && descMap.attrId == "001c") + { + map.name = "thermostatMode" + map.value = getModeMap()[descMap.value] + if (map.value == "cool") { + activeSetpoint = device.currentValue("coolingSetpoint") + } else if (map.value == "heat") { + activeSetpoint = device.currentValue("heatingSetpoint") + } + sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0011") + { + log.debug "COOL SET POINT" + map.name = "coolingSetpoint" + map.value = getTemperature(descMap.value) + if (device.currentState("thermostatMode")?.value == "cool") { + activeSetpoint = map.value + log.debug "Active set point value: $activeSetpoint" + sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) + } + } + else if (descMap.cluster == "0201" && descMap.attrId == "0012") + { + log.debug "HEAT SET POINT" + map.name = "heatingSetpoint" + map.value = getTemperature(descMap.value) + if (device.currentState("thermostatMode")?.value == "heat") { + activeSetpoint = map.value + sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) + } + } + else if (descMap.cluster == "0201" && descMap.attrId == "0029") + { + log.debug "OPERATING STATE" + map.name = "thermostatOperatingState" + map.value = getOperatingStateMap()[descMap.value] + } + + // Fan Control Cluster Attribute Read Response + else if (descMap.cluster == "0202" && descMap.attrId == "0000") + { + map.name = "thermostatFanMode" + map.value = getFanModeMap()[descMap.value] + } + + }// End of Read Attribute Response + + /*else if (description?.startsWith("updated")) { + configure() + } +*/ + def result = null + if (map) { + result = createEvent(map) + } + log.debug "Parse returned $map" + + return result +} + +// =============== Help Functions - Don't use log.debug in all these functins =============== +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +def getModeMap() { [ + "00":"off", + "03":"cool", + "04":"heat" +]} + +def getOperatingStateMap() { [ + "0000":"idle", + "0001":"heating", + "0002":"cooling", + "0004":"fan only", + "0005":"heating", + "0006":"cooling", + "0008":"heating", + "0009":"heating", + "000A":"heating", + "000D":"heating", + "0010":"cooling", + "0012":"cooling", + "0014":"cooling", + "0015":"cooling" +]} + +def getFanModeMap() { [ + "04":"fanOn", + "05":"fanAuto" +]} + +def getTemperatureDisplayModeMap() { [ + "00":"C", + "01":"F" +]} + + +def getTemperature(value) +{ + def decimalFormat = new java.text.DecimalFormat("#") + def celsius = Integer.parseInt(value, 16) / 100.0 as Double + def returnValue + + // Format to support decimal with one or two + decimalFormat.setMaximumFractionDigits(2) + decimalFormat.setMinimumFractionDigits(1) + + returnValue = decimalFormat.format(celsius); + + log.debug "Temperature value in C: $returnValue" + + if(getTemperatureScale() == "F"){ + returnValue = decimalFormat.format(Math.round(celsiusToFahrenheit(celsius)*10)/10.0) + + log.debug "Temperature value in F: $returnValue" + } + + return returnValue +} + + + +// =============== Setpoints =============== +def setpointUp() +{ + def currentMode = device.currentState("thermostatMode")?.value + def currentUnit = getTemperatureScale() + + // check if heating or cooling setpoint needs to be changed + double nextLevel = device.currentValue("thermostatSetpoint") + 1.0 + log.debug "Next level: $nextLevel" + + // check the limits + if(currentUnit == "C") + { + if (currentMode == "cool") + { + if(nextLevel > 36.0) + { + nextLevel = 36.0 + } + } else if (currentMode == "heat") + { + if(nextLevel > 32.0) + { + nextLevel = 32.0 + } + } + } + else //in degF unit + { + if (currentMode == "cool") + { + if(nextLevel > 96.0) + { + nextLevel = 96.0 + } + } else if (currentMode == "heat") + { + if(nextLevel > 89.0) + { + nextLevel = 89.0 + } + } + } + + log.debug "setpointUp() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}" + + setSetpoint(nextLevel) +} + +def setpointDown() +{ + def currentMode = device.currentState("thermostatMode")?.value + def currentUnit = getTemperatureScale() + + // check if heating or cooling setpoint needs to be changed + double nextLevel = device.currentValue("thermostatSetpoint") - 1.0 + + // check the limits + if (currentUnit == "C") + { + if (currentMode == "cool") + { + if(nextLevel < 8.0) + { + nextLevel = 8.0 + } + } else if (currentMode == "heat") + { + if(nextLevel < 10.0) + { + nextLevel = 10.0 + } + } + } + else //in degF unit + { + if (currentMode == "cool") + { + if (nextLevel < 47.0) + { + nextLevel = 47.0 + } + } else if (currentMode == "heat") + { + if (nextLevel < 50.0) + { + nextLevel = 50.0 + } + } + } + + log.debug "setpointDown() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}" + + setSetpoint(nextLevel) +} + + +def setSetpoint(degrees) +{ + def temperatureScale = getTemperatureScale() + def currentMode = device.currentState("thermostatMode")?.value + + def degreesDouble = degrees as Double + sendEvent("name":"thermostatSetpoint", "value":degreesDouble) + log.debug "New set point: $degreesDouble" + + def celsius = (getTemperatureScale() == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + if (currentMode == "cool") { + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}" + } + else if (currentMode == "heat") { + + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}" + } +} + +def setHeatingSetpoint(degrees) { + def temperatureScale = getTemperatureScale() + + def degreesDouble = degrees as Double + log.debug "setHeatingSetpoint({$degreesDouble} ${temperatureScale})" + sendEvent("name":"heatingSetpoint", "value":degreesDouble) + + def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}" +} + +def setCoolingSetpoint(degrees) { + def temperatureScale = getTemperatureScale() + + def degreesDouble = degrees as Double + log.debug "setCoolingSetpoint({$degreesDouble} ${temperatureScale})" + sendEvent("name":"coolingSetpoint", "value":degreesDouble) + + def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}" +} + +// =============== Thermostat Mode =============== +def modes() { + ["off", "heat", "cool"] +} + +def setThermostatMode() +{ + def currentMode = device.currentState("thermostatMode")?.value + def modeOrder = modes() + def index = modeOrder.indexOf(currentMode) + def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0] + log.debug "setThermostatMode - switching from $currentMode to $next" + "$next"() +} + +def setThermostatMode(String value) { + "$value"() +} + +def off() { + sendEvent("name":"thermostatMode", "value":"off") + sendEvent("name":"thermostatSetpoint","value":"--") + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}" +} + +def cool() { + def coolingSetpoint = device.currentValue("coolingSetpoint") + log.debug "Cool set point: $coolingSetpoint" + sendEvent("name":"thermostatMode", "value":"cool") + sendEvent("name":"thermostatSetpoint","value":coolingSetpoint) + [ + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {03}" + ] +} + +def heat() { + def heatingSetpoint = device.currentValue("heatingSetpoint") + log.debug "Heat set point: $heatingSetpoint" + sendEvent("name":"thermostatMode","value":"heat") + sendEvent("name":"thermostatSetpoint","value":heatingSetpoint) + [ + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}" + ] +} + +// =============== Fan Mode =============== +def setThermostatFanMode() +{ + def currentFanMode = device.currentState("thermostatFanMode")?.value + def returnCommand + + switch (currentFanMode) { + case "fanAuto": + returnCommand = fanOn() + break + case "fanOn": + returnCommand = fanAuto() + break + } + + if(!currentFanMode) { + returnCommand = fanAuto() + } + + log.debug "setThermostatFanMode - switching from $currentFanMode to $returnCommand" + + returnCommand +} + +def setThermostatFanMode(String value) { + "$value"() +} + +def on() { + fanOn() +} + +def fanOn() { + sendEvent("name":"thermostatFanMode", "value":"fanOn") + "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}" +} + +def auto() { + fanAuto() +} + +def fanAuto() { + sendEvent("name":"thermostatFanMode", "value":"fanAuto") + "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}" +} + + + + +// =============== SmartThings Default Fucntions: refresh, configure, poll =============== +def refresh() +{ + log.debug "refresh() - update attributes " + [ + + //Set long poll interval to 2 qs + "raw 0x0020 {11 00 02 02 00 00 00}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + //This is sent in this specific order to ensure that the temperature values are received after the unit/mode + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 800", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 800", + + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x11", "delay 800", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 800", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x29", "delay 800", + "st rattr 0x${device.deviceNetworkId} 1 0x202 0", "delay 800", + + //Set long poll interval to 28 qs (7 seconds) + "raw 0x0020 {11 00 02 1C 00 00 00}", + "send 0x${device.deviceNetworkId} 1 1" + ] +} + +def poll() +{ + refresh() +} + +def configure() +{ + log.debug "configure() - binding & attribute report" + [ + //Set long poll interval to 2 qs + "raw 0x0020 {11 00 02 02 00 00 00}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + //Thermostat - Cluster 201 + "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500", + + "zcl global send-me-a-report 0x201 0 0x29 5 300 {3200}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + "zcl global send-me-a-report 0x201 0x0011 0x29 5 300 {3200}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + "zcl global send-me-a-report 0x201 0x0012 0x29 5 300 {3200}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + "zcl global send-me-a-report 0x201 0x001C 0x30 5 300 {}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + "zcl global send-me-a-report 0x201 0x0029 0x19 5 300 {}", + "send 0x${device.deviceNetworkId} 1 1", "delay 500", + + //Fan Control - Cluster 202 + "zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}", "delay 500", + + "zcl global send-me-a-report 0x202 0 0x30 5 300 {}", + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + + ] + refresh() +} + + + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} diff --git a/smartapps/arno/bright-when-dark-and-or-bright-after-sunset.src/bright-when-dark-and-or-bright-after-sunset.groovy b/smartapps/arno/bright-when-dark-and-or-bright-after-sunset.src/bright-when-dark-and-or-bright-after-sunset.groovy new file mode 100644 index 00000000000..79764a8b9d7 --- /dev/null +++ b/smartapps/arno/bright-when-dark-and-or-bright-after-sunset.src/bright-when-dark-and-or-bright-after-sunset.groovy @@ -0,0 +1,762 @@ +definition( + name: "Bright When Dark And/Or Bright After Sunset", + namespace: "Arno", + author: "Arnaud", + description: "Turn ON light(s) and/or dimmer(s) when there's movement and the room is dark with illuminance threshold and/or between sunset and sunrise. Then turn OFF after X minute(s) when the brightness of the room is above the illuminance threshold or turn OFF after X minute(s) when there is no movement.", + category: "Convenience", + iconUrl: "http://neiloseman.com/wp-content/uploads/2013/08/stockvault-bulb128619.jpg", + iconX2Url: "http://neiloseman.com/wp-content/uploads/2013/08/stockvault-bulb128619.jpg" +) + +preferences +{ + page(name: "configurations") + page(name: "options") + + page(name: "timeIntervalInput", title: "Only during a certain time...") + { + section + { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def configurations() +{ + dynamicPage(name: "configurations", title: "Configurations...", uninstall: true, nextPage: "options") + { + section(title: "Turn ON lights on movement when...") + { + input "dark", "bool", title: "It is dark?", required: true + input "sun", "bool", title: "Between sunset and surise?", required: true + } + section(title: "More options...", hidden: hideOptionsSection(), hideable: true) + { + def timeLabel = timeIntervalLabel() + href "timeIntervalInput", title: "Only during a certain time:", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null + input "days", "enum", title: "Only on certain days of the week:", multiple: true, required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + input "modes", "mode", title: "Only when mode is:", multiple: true, required: false + } + section ("Assign a name") + { + label title: "Assign a name", required: false + } + } +} + +def options() +{ + if (dark == true && sun == true) + { + dynamicPage(name: "options", title: "Lights will turn ON on movement when it is dark and between sunset and sunrise...", install: true, uninstall: true) + { + section("Control these light(s)...") + { + input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false + } + section("Control these dimmer(s)...") + { + input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false + input "level", "number", title: "How bright?", required:false, description: "0% to 100%" + } + section("Turning ON when it's dark and there's movement...") + { + input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true + } + section("And then OFF when it's light or there's been no movement for...") + { + input "delayMinutes", "number", title: "Minutes?", required: false + } + section("Using this light sensor...") + { + input "lightSensor", "capability.illuminanceMeasurement",title: "Light Sensor?", multiple: false, required: true + input "luxLevel", "number", title: "Illuminance threshold? (default 50 lux)",defaultValue: "50", required: false + } + section ("And between sunset and sunrise...") + { + input "sunriseOffsetValue", "text", title: "Sunrise offset", required: false, description: "00:00" + input "sunriseOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]] + input "sunsetOffsetValue", "text", title: "Sunset offset", required: false, description: "00:00" + input "sunsetOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]] + } + section ("Zip code (optional, defaults to location coordinates when location services are enabled)...") + { + input "zipCode", "text", title: "Zip Code?", required: false, description: "Local Zip Code" + } + } + } + else if (dark == true && sun == false) + { + dynamicPage(name: "options", title: "Lights will turn ON on movement when it is dark...", install: true, uninstall: true) + { + section("Control these light(s)...") + { + input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false + } + section("Control these dimmer(s)...") + { + input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false + input "level", "number", title: "How bright?", required:false, description: "0% to 100%" + } + section("Turning ON when it's dark and there's movement...") + { + input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true + } + section("And then OFF when it's light or there's been no movement for...") + { + input "delayMinutes", "number", title: "Minutes?", required: false + } + section("Using this light sensor...") + { + input "lightSensor", "capability.illuminanceMeasurement",title: "Light Sensor?", multiple: false, required: true + input "luxLevel", "number", title: "Illuminance threshold? (default 50 lux)",defaultValue: "50", required: false + } + } + } + else if (sun == true && dark == false) + { + dynamicPage(name: "options", title: "Lights will turn ON on movement between sunset and sunrise...", install: true, uninstall: true) + { + section("Control these light(s)...") + { + input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false + } + section("Control these dimmer(s)...") + { + input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false + input "level", "number", title: "How bright?", required:false, description: "0% to 100%" + } + section("Turning ON there's movement...") + { + input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true + } + section("And then OFF there's been no movement for...") + { + input "delayMinutes", "number", title: "Minutes?", required: false + } + section ("Between sunset and sunrise...") + { + input "sunriseOffsetValue", "text", title: "Sunrise offset", required: false, description: "00:00" + input "sunriseOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]] + input "sunsetOffsetValue", "text", title: "Sunset offset", required: false, description: "00:00" + input "sunsetOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]] + } + section ("Zip code (optional, defaults to location coordinates when location services are enabled)...") + { + input "zipCode", "text", title: "Zip Code?", required: false, description: "Local Zip Code" + } + } + } + else + { + dynamicPage(name: "options", title: "Lights will turn ON on movement...", install: true, uninstall: true) + { + section("Control these light(s)...") + { + input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false + } + section("Control these dimmer(s)...") + { + input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false + input "level", "number", title: "How bright?", required:false, description: "0% to 100%" + } + section("Turning ON when there's movement...") + { + input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true + } + section("And then OFF when there's been no movement for...") + { + input "delayMinutes", "number", title: "Minutes?", required: false + } + } + } +} + +def installed() +{ + log.debug "Installed with settings: ${settings}." + initialize() +} + +def updated() +{ + log.debug "Updated with settings: ${settings}." + unsubscribe() + unschedule() + initialize() +} + +def initialize() +{ + subscribe(motionSensor, "motion", motionHandler) + if (lights != null && lights != "" && dimmers != null && dimmers != "") + { + log.debug "$lights subscribing..." + subscribe(lights, "switch", lightsHandler) + log.debug "$dimmers subscribing..." + subscribe(dimmers, "switch", dimmersHandler) + if (dark == true && lightSensor != null && lightSensor != "") + { + log.debug "$lights and $dimmers will turn ON when movement detected and when it is dark..." + subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false]) + } + if (sun == true) + { + log.debug "$lights and $dimmers will turn ON when movement detected between sunset and sunrise..." + astroCheck() + subscribe(location, "position", locationPositionChange) + subscribe(location, "sunriseTime", sunriseSunsetTimeHandler) + subscribe(location, "sunsetTime", sunriseSunsetTimeHandler) + } + else if (dark != true && sun != true) + { + log.debug "$lights and $dimmers will turn ON when movement detected..." + } + } + else if (lights != null && lights != "") + { + log.debug "$lights subscribing..." + subscribe(lights, "switch", lightsHandler) + if (dark == true && lightSensor != null && lightSensor != "") + { + log.debug "$lights will turn ON when movement detected and when it is dark..." + subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false]) + } + if (sun == true) + { + log.debug "$lights will turn ON when movement detected between sunset and sunrise..." + astroCheck() + subscribe(location, "position", locationPositionChange) + subscribe(location, "sunriseTime", sunriseSunsetTimeHandler) + subscribe(location, "sunsetTime", sunriseSunsetTimeHandler) + } + else if (dark != true && sun != true) + { + log.debug "$lights will turn ON when movement detected..." + } + } + else if (dimmers != null && dimmers != "") + { + log.debug "$dimmers subscribing..." + subscribe(dimmers, "switch", dimmersHandler) + if (dark == true && lightSensor != null && lightSensor != "") + { + log.debug "$dimmers will turn ON when movement detected and when it is dark..." + subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false]) + } + if (sun == true) + { + log.debug "$dimmers will turn ON when movement detected between sunset and sunrise..." + astroCheck() + subscribe(location, "position", locationPositionChange) + subscribe(location, "sunriseTime", sunriseSunsetTimeHandler) + subscribe(location, "sunsetTime", sunriseSunsetTimeHandler) + } + else if (dark != true && sun != true) + { + log.debug "$dimmers will turn ON when movement detected..." + } + } + log.debug "Determinating lights and dimmers current value..." + if (lights != null && lights != "") + { + if (lights.currentValue("switch").toString().contains("on")) + { + state.lightsState = "on" + log.debug "Lights $state.lightsState." + } + else if (lights.currentValue("switch").toString().contains("off")) + { + state.lightsState = "off" + log.debug "Lights $state.lightsState." + } + else + { + log.debug "ERROR!" + } + } + if (dimmers != null && dimmers != "") + { + if (dimmers.currentValue("switch").toString().contains("on")) + { + state.dimmersState = "on" + log.debug "Dimmers $state.dimmersState." + } + else if (dimmers.currentValue("switch").toString().contains("off")) + { + state.dimmersState = "off" + log.debug "Dimmers $state.dimmersState." + } + else + { + log.debug "ERROR!" + } + } +} + +def locationPositionChange(evt) +{ + log.trace "locationChange()" + astroCheck() +} + +def sunriseSunsetTimeHandler(evt) +{ + state.lastSunriseSunsetEvent = now() + log.debug "SmartNightlight.sunriseSunsetTimeHandler($app.id)" + astroCheck() +} + +def motionHandler(evt) +{ + log.debug "$evt.name: $evt.value" + if (evt.value == "active") + { + unschedule(turnOffLights) + unschedule(turnOffDimmers) + if (dark == true && sun == true) + { + if (darkOk == true && sunOk == true) + { + log.debug "Lights and Dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark or because $motionSensor detected motion between sunset and sunrise..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn ON..." + turnOnLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn ON..." + turnOnDimmers() + } + } + else if (darkOk == true && sunOk != true) + { + log.debug "Lights and Dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn ON..." + turnOnLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn ON..." + turnOnDimmers() + } + } + else if (darkOk != true && sunOk == true) + { + log.debug "Lights and dimmers will turn ON because $motionSensor detected motion between sunset and sunrise..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn ON..." + turnOnLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn ON..." + turnOnDimmers() + } + } + else + { + log.debug "Lights and dimmers will not turn ON because $lightSensor is too bright or because time not between sunset and surise." + } + } + else if (dark == true && sun != true) + { + if (darkOk == true) + { + log.debug "Lights and dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn ON..." + turnOnLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn ON..." + turnOnDimmers() + } + } + else + { + log.debug "Lights and dimmers will not turn ON because $lightSensor is too bright." + } + } + else if (dark != true && sun == true) + { + if (sunOk == true) + { + log.debug "Lights and dimmers will turn ON because $motionSensor detected motion between sunset and sunrise..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn ON..." + turnOnLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn ON..." + turnOnDimmers() + } + } + else + { + log.debug "Lights and dimmers will not turn ON because time not between sunset and surise." + } + } + else if (dark != true && sun != true) + { + log.debug "Lights and dimmers will turn ON because $motionSensor detected motion..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn ON..." + turnOnLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn ON..." + turnOnDimmers() + } + } + } + else if (evt.value == "inactive") + { + unschedule(turnOffLights) + unschedule(turnOffDimmers) + if (state.lightsState != "off" || state.dimmersState != "off") + { + log.debug "Lights and/or dimmers are not OFF." + if (delayMinutes) + { + def delay = delayMinutes * 60 + if (dark == true && sun == true) + { + log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) after turning ON when dark or between sunset and sunrise..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffLights) + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffDimmers) + } + } + else if (dark == true && sun != true) + { + log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) after turning ON when dark..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffLights) + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffDimmers) + } + } + else if (dark != true && sun == true) + { + log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) between sunset and sunrise..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffLights) + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffDimmers) + } + } + else if (dark != true && sun != true) + { + log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s)..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffLights) + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..." + runIn(delay, turnOffDimmers) + } + } + } + else + { + log.debug "Lights and dimmers will stay ON because no turn OFF delay was set..." + } + } + else if (state.lightsState == "off" && state.dimmersState == "off") + { + log.debug "Lights and dimmers are already OFF and will not turn OFF in $delayMinutes minute(s)." + } + } +} + +def lightsHandler(evt) +{ + log.debug "Lights Handler $evt.name: $evt.value" + if (evt.value == "on") + { + log.debug "Lights: $lights now ON." + unschedule(turnOffLights) + state.lightsState = "on" + } + else if (evt.value == "off") + { + log.debug "Lights: $lights now OFF." + unschedule(turnOffLights) + state.lightsState = "off" + } +} + +def dimmersHandler(evt) +{ + log.debug "Dimmer Handler $evt.name: $evt.value" + if (evt.value == "on") + { + log.debug "Dimmers: $dimmers now ON." + unschedule(turnOffDimmers) + state.dimmersState = "on" + } + else if (evt.value == "off") + { + log.debug "Dimmers: $dimmers now OFF." + unschedule(turnOffDimmers) + state.dimmersState = "off" + } +} + +def illuminanceHandler(evt) +{ + log.debug "$evt.name: $evt.value, lastStatus lights: $state.lightsState, lastStatus dimmers: $state.dimmersState, motionStopTime: $state.motionStopTime" + unschedule(turnOffLights) + unschedule(turnOffDimmers) + if (evt.integerValue > 999) + { + log.debug "Lights and dimmers will turn OFF because illuminance is superior to 999 lux..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn OFF..." + turnOffLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn OFF..." + turnOffDimmers() + } + } + else if (evt.integerValue > ((luxLevel != null && luxLevel != "") ? luxLevel : 50)) + { + log.debug "Lights and dimmers will turn OFF because illuminance is superior to $luxLevel lux..." + if (lights != null && lights != "") + { + log.debug "Lights: $lights will turn OFF..." + turnOffLights() + } + if (dimmers != null && dimmers != "") + { + log.debug "Dimmers: $dimmers will turn OFF..." + turnOffDimmers() + } + } +} + +def turnOnLights() +{ + if (allOk) + { + if (state.lightsState != "on") + { + log.debug "Turning ON lights: $lights..." + lights?.on() + state.lightsState = "on" + } + else + { + log.debug "Lights: $lights already ON." + } + } + else + { + log.debug "Time, days of the week or mode out of range! $lights will not turn ON." + } +} + +def turnOnDimmers() +{ + if (allOk) + { + if (state.dimmersState != "on") + { + log.debug "Turning ON dimmers: $dimmers..." + settings.dimmers?.setLevel(level) + state.dimmersState = "on" + } + else + { + log.debug "Dimmers: $dimmers already ON." + } + } + else + { + log.debug "Time, days of the week or mode out of range! $dimmers will not turn ON." + } +} + + +def turnOffLights() +{ + if (allOk) + { + if (state.lightsState != "off") + { + log.debug "Turning OFF lights: $lights..." + lights?.off() + state.lightsState = "on" + } + else + { + log.debug "Lights: $lights already OFF." + } + } + else + { + log.debug "Time, day of the week or mode out of range! $lights will not turn OFF." + } +} + +def turnOffDimmers() +{ + if (allOk) + { + if (state.dimmersState != "off") + { + log.debug "Turning OFF dimmers: $dimmers..." + dimmers?.off() + state.dimmersState = "off" + } + else + { + log.debug "Dimmers: $dimmers already OFF." + } + } + else + { + log.debug "Time, day of the week or mode out of range! $dimmers will not turn OFF." + } +} + +def astroCheck() +{ + def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) + state.riseTime = s.sunrise.time + state.setTime = s.sunset.time + log.debug "Sunrise: ${new Date(state.riseTime)}($state.riseTime), Sunset: ${new Date(state.setTime)}($state.setTime)" +} + +private getDarkOk() +{ + def result + if (dark == true && lightSensor != null && lightSensor != "") + { + result = lightSensor.currentIlluminance < ((luxLevel != null && luxLevel != "") ? luxLevel : 50) + } + log.trace "darkOk = $result" + result +} + +private getSunOk() +{ + def result + if (sun == true) + { + def t = now() + result = t < state.riseTime || t > state.setTime + } + log.trace "sunOk = $result" + result +} + +private getSunriseOffset() +{ + sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null +} + +private getSunsetOffset() +{ + sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null +} + +private getAllOk() +{ + modeOk && daysOk && timeOk +} + +private getModeOk() +{ + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() +{ + def result = true + if (days) + { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) + { + df.setTimeZone(location.timeZone) + } + else + { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() +{ + def result = true + if (starting && ending) + { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private hideOptionsSection() +{ + (starting || ending || days || modes) ? false : true +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} diff --git a/smartapps/charette-joseph-gmail-com/good-night-house.src/good-night-house.groovy b/smartapps/charette-joseph-gmail-com/good-night-house.src/good-night-house.groovy new file mode 100644 index 00000000000..176a336f9b9 --- /dev/null +++ b/smartapps/charette-joseph-gmail-com/good-night-house.src/good-night-house.groovy @@ -0,0 +1,80 @@ +/** + * Good Night House + * + * Copyright 2014 Joseph Charette + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Good Night House", + namespace: "charette.joseph@gmail.com", + author: "Joseph Charette", + description: "Some on, some off with delay for bedtime, Lock The Doors", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" +/** +* Borrowed code from +* Walk Gentle Into That Good Night +* +* Author: oneaccttorulethehouse@gmail.com +* Date: 2014-02-01 + */ + ) +preferences { + section("When I touch the app turn these lights off…"){ + input "switchesoff", "capability.switch", multiple: true, required:true + } + section("When I touch the app turn these lights on…"){ + input "switcheson", "capability.switch", multiple: true, required:false + } + section("Lock theses locks...") { + input "lock1","capability.lock", multiple: true + } + section("And change to this mode...") { + input "newMode", "mode", title: "Mode?" + } + section("After so many seconds (optional)"){ + input "waitfor", "number", title: "Off after (default 120)", required: true + } +} + + +def installed() +{ + log.debug "Installed with settings: ${settings}" + log.debug "Current mode = ${location.mode}" + subscribe(app, appTouch) +} + + +def updated() +{ + log.debug "Updated with settings: ${settings}" + log.debug "Current mode = ${location.mode}" + unsubscribe() + subscribe(app, appTouch) +} + +def appTouch(evt) { + log.debug "changeMode, location.mode = $location.mode, newMode = $newMode, location.modes = $location.modes" + if (location.mode != newMode) { + setLocationMode(newMode) + log.debug "Changed the mode to '${newMode}'" + } else { + log.debug "New mode is the same as the old mode, leaving it be" + } + log.debug "appTouch: $evt" + lock1.lock() + switcheson.on() + def delay = (waitfor != null && waitfor != "") ? waitfor * 1000 : 120000 + switchesoff.off(delay: delay) +} diff --git a/smartapps/chrisb/goodnight-ubi.src/goodnight-ubi.groovy b/smartapps/chrisb/goodnight-ubi.src/goodnight-ubi.groovy new file mode 100644 index 00000000000..ede8454f0ac --- /dev/null +++ b/smartapps/chrisb/goodnight-ubi.src/goodnight-ubi.groovy @@ -0,0 +1,117 @@ +/** + * Goodnight Ubi + * + * Copyright 2014 Christopher Boerma + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Goodnight Ubi", + namespace: "chrisb", + author: "chrisb", + description: "An app to coordinate bedtime activities between Ubi and SmartThings. This app will activate when a Virtual Tile is triggers (Setup custom behavior in Ubi to turn on this tile when you say goodnight to ubi). This app will then turn off selected lights after a specified number of minutes. It will also check if any doors or windows are open. If they are, Ubi will tell you which ones are open. Finally, the app will say goodnight to hello home if requested.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + + +preferences { + section("Enter Ubi information:") { + input "behaviorToken", "text", title: "What is the Ubi Token?", required: true, autoCorrect:false + // Get token from the Ubi Portal. Select HTTP request as trigger and token will be displayed. + input "trigger", "capability.switch", title: "Which virtual tile is the trigger?", required: true + // Create a Virtual on/off button tile for this. + } + + section("Which doors and windows should I check?"){ + input "doors", "capability.contactSensor", multiple: true + } + + section("Which light switches will I be turning off?") { + input "theSwitches", "capability.switch", Title: "Which?", multiple: true, required: false + input "minutes", "number", Title: "After how many minutes?", required: true + } + section("Should I say 'Goodnight' to Hello Home?") { + input "sayPhrase", "enum", metadata:[values:["Yes","No"]] + } +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + +initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(trigger, "switch.on", switchOnHandler) // User should set up on Ubi that when the choosen +} // trigger is said, Ubi turns on this virtual switch. + +def switchOnHandler(evt) { + log.debug "trigger turned on!" + + def timeDelay = minutes * 60 // convert minutes to seconds. + runIn (timeDelay, lightsOut) // schedule the lights out procedure + + def phrase = "" // Make sure Phrase is empty at the start of each run. + + doors.each { doorOpen -> // cycles through all contact sensor devices selected + if (doorOpen.currentContact == "open") { // if the current selected device is open, then: + log.debug "$doorOpen.displayName" // echo to the simulator the device's name + def toReplace = doorOpen.displayName // make variable 'toReplace' = the devices name. + def replaced = toReplace.replaceAll(' ', '%20') // make variable 'replaced' = 'toReplace' with all the space changed to %20 + log.debug replaced // echo to the simulator the new name. + + phrase = phrase.replaceAll('%20And%20', '%20') // Remove any previously added "and's" to make it sound natural. + + if (phrase == "") { // If Phrase is empty (ie, this is the first name to be added)... + phrase = "The%20" + replaced // ...then add "The%20" plus the device name. + } else { // If Phrase isn't empty... + phrase = phrase + ",%20And%20The%20" + replaced // ...then add ",%20And%20The%20". + } + + log.debug phrase // Echo the current version of 'Phrase' + } // Closes the IF statement. + } // Closes the doors.each cycle + + if (phrase == "") { + phrase = "The%20house%20is%20ready%20for%20night." + } + else { + phrase = "You%20have%20left%20" + phrase + "open" + } + + httpGet("https://portal.theubi.com/webapi/behaviour?access_token=${behaviorToken}&variable=${phrase}") + // send the http request and push the device name (replaced) as the variable. + // On the Ubi side you need to setup a custom behavior (which you've already done to get the token) + // and have say something like: "Hold on! The ${variable} is open!" Ubi will then take 'replaced' + // from this http request and insert it into the phrase that it says. + + if (sayPhrase == "Yes") { // If the user selected to say Goodnight... + location.helloHome.execute("Good Night!") // ...say goodnight to Hello Home. + } +} // Close the switchOnHandler Process + +def lightsOut() { + log.debug "Turning off trigger" + trigger.off() // Turn off the trigger tile button for next run + if (theSwitches == "") {} else { // If the user didn't enter any light to turn off, do nothing... + log.debug "Turning off switches" // ...but if the user did enter lights, then turn them + theSwitches.off() // off here. + } +} diff --git a/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy b/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy new file mode 100644 index 00000000000..f822ee37fdc --- /dev/null +++ b/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2014 Andrew Reitz + * + * 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. + */ + +/** + * Jenkins Notifier + * + * Checks a Jenkins server at a specific time, if the build fails it will turn on a light. If the build goes from + * failing back to succeeding the light will turn off. Hues can also be used in place of the light in order to create + * colors for build statuses + */ + +// Automatically generated. Make future change here. +definition( + name: "Jenkins Notifier", + namespace: "com.andrewreitz", + author: "aj.reitz@gmail.com", + description: "Turn off and on devices based on the state that your Jenkins Build is in.", + category: "Fun & Social", + iconUrl: "http://i.imgur.com/tyIp8wQ.jpg", + iconX2Url: "http://i.imgur.com/tyIp8wQ.jpg" +) + +preferences { + section("The URL to your Jenkins, including the job you want to monitor. Ex. https://jenkins.example.com/job/myproject/") { + input "jenkinsUrl", "text", title: "Jenkins URL" + } + section("Jenkins Username") { + input "jenkinsUsername", "text", title: "Jenkins Username" + } + section("Jenkins Password") { + input "jenkinsPassword", "password", title: "Jenkins Password" + } + section("On Failed Build Turn On...") { + input "switches", "capability.switch", multiple: true, required: false + } + section("Or Change These Bulbs...") { + input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required: false, multiple: true + input "colorSuccess", "enum", title: "Hue Color On Success?", required: false, multiple: false, options: getHueColors().keySet() as String[] + input "colorFail", "enum", title: "Hue Color On Fail?", required: false, multiple: false, options: getHueColors().keySet() as String[] + input "lightLevelSuccess", "number", title: "Light Level On Success?", required: false + input "lightLevelFail", "number", title: "Light Level On Fail?", required: false + } + section("Additional settings", hideable: true, hidden: true) { + paragraph("Default check time is 15 Minutes") + input "refreshInterval", "decimal", title: "Check Server... (minutes)", + description: "Enter time in minutes", defaultValue: 15, required: false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +/** Constants for Hue Colors */ +Map getHueColors() { + return [Red: 0, Green: 39, Blue: 70, Yellow: 25, Orange: 10, Purple: 75, Pink: 83] +} + +/** Constant for Saturation */ +int getSaturation() { + return 100; +} + +/** Constant for Level */ +int getMaxLevel() { + return 100; +} + +def initialize() { + def successColor = [switch: "on", hue: getHueColors()[colorSuccess], saturation: getSaturation(), level: lightLevelSuccess ?: getMaxLevel()] + def failColor = [switch: "on", hue: getHueColors()[colorFail], saturation: getSaturation(), level: lightLevelFail ?: getMaxLevel()] + state.successColor = successColor + state.failColor = failColor + log.debug "successColor: ${successColor}, failColor: ${failColor}" + + checkServer() + + def cron = "* */${refreshInterval ?: 15} * * * ?" + schedule(cron, checkServer) +} + +def checkServer() { + log.debug "Checking Server Now" + + def successColor = state.successColor + def failColor = state.failColor + + def basicCredentials = "${jenkinsUsername}:${jenkinsPassword}" + def encodedCredentials = basicCredentials.encodeAsBase64().toString() + def basicAuth = "Basic ${encodedCredentials}" + + def head = ["Authorization": basicAuth] + + log.debug "Auth ${head}" + + def host = jenkinsUrl.contains("lastBuild/api/json") ? jenkinsUrl : "${jenkinsUrl}/lastBuild/api/json" + + httpGet(uri: host, headers: ["Authorization": "${basicAuth}"]) { resp -> + def buildError = (resp.data.result == "FAILURE") + def buildSuccess = (resp.data.result == "SUCCESS") + log.debug "Build Success? ${buildSuccess}" + if (buildError) { + switches?.on() + hues?.setColor(failColor) + } else if (buildSuccess) { + switches?.off() + hues?.setColor(successColor) + } // else in some other state, probably building, do nothing. + + } +} diff --git a/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy b/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy new file mode 100644 index 00000000000..bad8f992e21 --- /dev/null +++ b/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy @@ -0,0 +1,76 @@ +/** + * ObyThing Music SmartApp + * + * Copyright 2014 obycode + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "ObyThing Music (Connect)", + namespace: "com.obycode", + author: "obycode", + description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.", + category: "SmartThings Labs", + iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png", + iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png") + + +preferences { + section("Get the IP address and port for your Mac computer using the ObyThing App (http://obything.obycode.com) and set up the SmartApp below:") { + input "theAddr", "string", title: "IP:port (click icon in status bar)", multiple: false, required: true + } + section("on this hub...") { + input "theHub", "hub", multiple: false, required: true + } + +} + +def installed() { + log.debug "Installed ${app.label} with address '${settings.theAddr}' on hub '${settings.theHub.name}'" + + initialize() +} + +def updated() { + /* + log.debug "Updated ${app.label} with address '${settings.theAddr}' on hub '${settings.theHub.name}'" + + def current = getChildDevices() + log.debug "children: $current" + + if (app.label != current.label) { + log.debug "CHANGING name from ${current.label} to ${app.label}" + log.debug "label props: ${current.label.getProperties()}" + current.label[0] = app.label + } + */ +} + +def initialize() { + def parts = theAddr.split(":") + def iphex = convertIPtoHex(parts[0]) + def porthex = convertPortToHex(parts[1]) + def dni = "$iphex:$porthex" + def hubNames = location.hubs*.name.findAll { it } + def d = addChildDevice("com.obycode", "ObyThing Music", dni, theHub.id, [label:"${app.label}", name:"ObyThing"]) + log.trace "created ObyThing '${d.displayName}' with id $dni" +} + +private String convertIPtoHex(ipAddress) { + String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02X', it.toInteger() ) }.join() + return hex + +} + +private String convertPortToHex(port) { + String hexport = port.toString().format( '%04X', port.toInteger() ) + return hexport +} diff --git a/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy b/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy new file mode 100644 index 00000000000..d804c070c9c --- /dev/null +++ b/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy @@ -0,0 +1,125 @@ +/** + * Working From Home + * + * Copyright 2014 George Sudarkoff + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Working From Home", + namespace: "com.sudarkoff", + author: "George Sudarkoff", + description: "If after a particular time of day a certain person is still at home, trigger a 'Working From Home' action.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic@2x.png" +) + +preferences { + page (name:"configActions") +} + +def configActions() { + dynamicPage(name: "configActions", title: "Configure Actions", uninstall: true, install: true) { + section ("When this person") { + input "person", "capability.presenceSensor", title: "Who?", multiple: false, required: true + } + section ("Still at home past") { + input "timeOfDay", "time", title: "What time?", required: true + } + + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + section("Perform this action") { + input "wfhPhrase", "enum", title: "\"Hello, Home\" action", required: true, options: phrases + } + } + + section (title: "More options", hidden: hideOptions(), hideable: true) { + input "sendPushMessage", "bool", title: "Send a push notification?" + input "phone", "phone", title: "Send a Text Message?", required: false + input "days", "enum", title: "Set for specific day(s) of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + } + + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +def installed() { + initialize() +} + +def updated() { + unschedule() + initialize() +} + +def initialize() { + schedule(timeToday(timeOfDay, location?.timeZone), "checkPresence") + if (customName) { + app.setTitle(customName) + } +} + +def checkPresence() { + if (daysOk && modeOk) { + if (person.latestValue("presence") == "present") { + log.debug "${person} is present, triggering WFH action." + location.helloHome.execute(settings.wfhPhrase) + def message = "${location.name} executed '${settings.wfhPhrase}' because ${person} is home." + send(message) + } + } +} + +private send(msg) { + if (sendPushMessage != "No") { + sendPush(msg) + } + + if (phone) { + sendSms(phone, msg) + } + + log.debug msg +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + result +} + +private hideOptions() { + (days || modes)? false: true +} + diff --git a/smartapps/curb/curb-control.src/curb-control.groovy b/smartapps/curb/curb-control.src/curb-control.groovy new file mode 100644 index 00000000000..3a1c83d922f --- /dev/null +++ b/smartapps/curb/curb-control.src/curb-control.groovy @@ -0,0 +1,100 @@ +/** + * Curb Control + * + * Author: Curb + */ + +definition( + name: "Curb Control", + namespace: "Curb", + author: "Curb", + description: "This SmartApp allows you to interact with the switches in your physical graph through Curb.", + category: "Convenience", + iconUrl: "http://energycurb.com/images/logo.png", + iconX2Url: "http://energycurb.com/images/logo.png", + oauth: [displayName: "SmartThings Curb Control", displayLink: "energycurb.com"] +) + +preferences { + section("Allow Curb to Control These Things...") { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + } +} + +mappings { + path("/") { + action: [ + GET: "index" + ] + } + path("/switches") { + action: [ + GET: "listSwitches", + PUT: "updateSwitches" + ] + } + path("/switches/:id") { + action: [ + GET: "showSwitch", + PUT: "updateSwitch" + ] + } +} + +def installed() {} + +def updated() {} + +def index(){ + [[url: "/switches"]] +} + +def listSwitches() { + switches.collect { device(it,"switch") } +} +void updateSwitches() { + updateAll(switches) +} +def showSwitch() { + show(switches, "switch") +} +void updateSwitch() { + update(switches) +} + +private void updateAll(devices) { + def command = request.JSON?.command + if (command) { + devices."$command"() + } +} + +private void update(devices) { + log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" + def command = request.JSON?.command + if (command) { + def device = devices.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + device."$command"() + } + } +} + +private show(devices, name) { + def d = devices.find { it.id == params.id } + if (!d) { + httpError(404, "Device not found") + } + else { + device(d, name) + } +} + +private device(it, name){ + if(it) { + def s = it.currentState(name) + [id: it.id, label: it.displayName, name: it.displayName, state: s] + } +} diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy new file mode 100644 index 00000000000..8260360fe79 --- /dev/null +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -0,0 +1,597 @@ +/** + * Netatmo Connect + */ +import java.text.DecimalFormat +import groovy.json.JsonSlurper + +private apiUrl() { "https://api.netatmo.com" } +private getVendorName() { "netatmo" } +private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" } +private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" } +private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" } +private getClientId() { appSettings.clientId } +private getClientSecret() { appSettings.clientSecret } +private getServerUrl() { "https://graph.api.smartthings.com" } + +// Automatically generated. Make future change here. +definition( + name: "Netatmo (Connect)", + namespace: "dianoga", + author: "Brian Steere", + description: "Netatmo Integration", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", + oauth: true +){ + appSetting "clientId" + appSetting "clientSecret" +} + +preferences { + page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false) + page(name: "listDevices", title: "Netatmo Devices", content: "listDevices", install: false) +} + +mappings { + path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]} + path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]} + path("/auth"){action: [GET: "auth"]} +} + +def authPage() { + log.debug "In authPage" + if(canInstallLabs()) { + def description = null + + if (state.vendorAccessToken == null) { + log.debug "About to create access token." + + createAccessToken() + description = "Tap to enter Credentials." + + return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) { + section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description } + } + } else { + description = "Tap 'Next' to proceed" + + return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) { + section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } + } + } + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + + return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section { + paragraph "$upgradeNeeded" + } + } + + } +} + +def auth() { + redirect location: oauthInitUrl() +} + +def oauthInitUrl() { + log.debug "In oauthInitUrl" + + /* OAuth Step 1: Request access code with our client ID */ + + state.oauthInitState = UUID.randomUUID().toString() + + def oauthParams = [ response_type: "code", + client_id: getClientId(), + state: state.oauthInitState, + redirect_uri: buildRedirectUrl("receiveToken") , + scope: "read_station" + ] + + return getVendorAuthPath() + toQueryString(oauthParams) +} + +def buildRedirectUrl(endPoint) { + log.debug "In buildRedirectUrl" + + return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}" +} + +def receiveToken() { + log.debug "In receiveToken" + + def oauthParams = [ + client_secret: getClientSecret(), + client_id: getClientId(), + grant_type: "authorization_code", + redirect_uri: buildRedirectUrl('receiveToken'), + code: params.code, + scope: "read_station" + ] + + def tokenUrl = getVendorTokenPath() + def params = [ + uri: tokenUrl, + contentType: 'application/x-www-form-urlencoded', + body: oauthParams, + ] + + log.debug params + + /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ + try { + httpPost(params) { response -> + log.debug response.data + def slurper = new JsonSlurper(); + + response.data.each {key, value -> + def data = slurper.parseText(key); + log.debug "Data: $data" + + state.vendorRefreshToken = data.refresh_token + state.vendorAccessToken = data.access_token + state.vendorTokenExpires = now() + (data.expires_in * 1000) + return + } + + } + } catch (Exception e) { + log.debug "Error: $e" + } + + log.debug "State: $state" + + if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install + return + } + + /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */ + + def html = """ + + + + + ${getVendorName()} Connection + + + +

+ Vendor icon + connected device icon + SmartThings logo +

We have located your """ + getVendorName() + """ account.

+

Tap 'Done' to process your credentials.

+
+ + + """ + render contentType: 'text/html', data: html +} + +def receivedToken() { + log.debug "In receivedToken" + + def html = """ + + + + + Withings Connection + + + +
+ Vendor icon + connected device icon + SmartThings logo +

Tap 'Done' to continue to Devices.

+
+ + + """ + render contentType: 'text/html', data: html +} + +// " + +def refreshToken() { + log.debug "In refreshToken" + + def oauthParams = [ + client_secret: getClientSecret(), + client_id: getClientId(), + grant_type: "refresh_token", + refresh_token: state.vendorRefreshToken + ] + + def tokenUrl = getVendorTokenPath() + def params = [ + uri: tokenUrl, + contentType: 'application/x-www-form-urlencoded', + body: oauthParams, + ] + + /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ + try { + httpPost(params) { response -> + def slurper = new JsonSlurper(); + + response.data.each {key, value -> + def data = slurper.parseText(key); + log.debug "Data: $data" + + state.vendorRefreshToken = data.refresh_token + state.vendorAccessToken = data.access_token + state.vendorTokenExpires = now() + (data.expires_in * 1000) + return true + } + + } + } catch (Exception e) { + log.debug "Error: $e" + } + + log.debug "State: $state" + + if ( !state.vendorAccessToken ) { //We didn't get an access token + return false + } +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + unschedule() + initialize() +} + +def initialize() { + log.debug "Initialized with settings: ${settings}" + + // Pull the latest device info into state + getDeviceList(); + + settings.devices.each { + def deviceId = it + def detail = state.deviceDetail[deviceId] + + try { + switch(detail.type) { + case 'NAMain': + log.debug "Base station" + createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name) + break + case 'NAModule1': + log.debug "Outdoor module" + createChildDevice("Netatmo Outdoor Module", deviceId, "${detail.type}.${deviceId}", detail.module_name) + break + case 'NAModule3': + log.debug "Rain Gauge" + createChildDevice("Netatmo Rain", deviceId, "${detail.type}.${deviceId}", detail.module_name) + break + case 'NAModule4': + log.debug "Additional module" + createChildDevice("Netatmo Additional Module", deviceId, "${detail.type}.${deviceId}", detail.module_name) + break + } + } catch (Exception e) { + log.error "Error creating device: ${e}" + } + } + + // Cleanup any other devices that need to go away + def delete = getChildDevices().findAll { !settings.devices.contains(it.deviceNetworkId) } + log.debug "Delete: $delete" + delete.each { deleteChildDevice(it.deviceNetworkId) } + + // Do the initial poll + poll() + // Schedule it to run every 5 minutes + runEvery5Minutes("poll") +} + +def uninstalled() { + log.debug "In uninstalled" + + removeChildDevices(getChildDevices()) +} + +def getDeviceList() { + log.debug "In getDeviceList" + + def deviceList = [:] + state.deviceDetail = [:] + state.deviceState = [:] + + apiGet("/api/devicelist") { response -> + response.data.body.devices.each { value -> + def key = value._id + deviceList[key] = "${value.station_name}: ${value.module_name}" + state.deviceDetail[key] = value + state.deviceState[key] = value.dashboard_data + } + response.data.body.modules.each { value -> + def key = value._id + deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}" + state.deviceDetail[key] = value + state.deviceState[key] = value.dashboard_data + } + } + + return deviceList.sort() { it.value.toLowerCase() } +} + +private removeChildDevices(delete) { + log.debug "In removeChildDevices" + + log.debug "deleting ${delete.size()} devices" + + delete.each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def createChildDevice(deviceFile, dni, name, label) { + log.debug "In createChildDevice" + + try { + def existingDevice = getChildDevice(dni) + if(!existingDevice) { + log.debug "Creating child" + def childDevice = addChildDevice("dianoga", deviceFile, dni, null, [name: name, label: label, completedSetup: true]) + } else { + log.debug "Device $dni already exists" + } + } catch (e) { + log.error "Error creating device: ${e}" + } +} + +def listDevices() { + log.debug "In listDevices" + + def devices = getDeviceList() + + dynamicPage(name: "listDevices", title: "Choose devices", install: true) { + section("Devices") { + input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices + } + + section("Preferences") { + input "rainUnits", "enum", title: "Rain Units", description: "Millimeters (mm) or Inches (in)", required: true, options: [mm:'Millimeters', in:'Inches'] + } + } +} + +def apiGet(String path, Map query, Closure callback) { + if(now() >= state.vendorTokenExpires) { + refreshToken(); + } + + query['access_token'] = state.vendorAccessToken + def params = [ + uri: apiUrl(), + path: path, + 'query': query + ] + // log.debug "API Get: $params" + + try { + httpGet(params) { response -> + callback.call(response) + } + } catch (Exception e) { + // This is most likely due to an invalid token. Try to refresh it and try again. + log.debug "apiGet: Call failed $e" + if(refreshToken()) { + log.debug "apiGet: Trying again after refreshing token" + httpGet(params) { response -> + callback.call(response) + } + } + } +} + +def apiGet(String path, Closure callback) { + apiGet(path, [:], callback); +} + +def poll() { + log.debug "In Poll" + getDeviceList(); + def children = getChildDevices() + log.debug "State: ${state.deviceState}" + + settings.devices.each { deviceId -> + def detail = state.deviceDetail[deviceId] + def data = state.deviceState[deviceId] + def child = children.find { it.deviceNetworkId == deviceId } + + log.debug "Update: $child"; + switch(detail.type) { + case 'NAMain': + log.debug "Updating NAMain $data" + child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale()) + child?.sendEvent(name: 'carbonDioxide', value: data['CO2']) + child?.sendEvent(name: 'humidity', value: data['Humidity']) + child?.sendEvent(name: 'pressure', value: data['Pressure']) + child?.sendEvent(name: 'noise', value: data['Noise']) + break; + case 'NAModule1': + log.debug "Updating NAModule1 $data" + child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale()) + child?.sendEvent(name: 'humidity', value: data['Humidity']) + break; + case 'NAModule3': + log.debug "Updating NAModule3 $data" + child?.sendEvent(name: 'rain', value: rainToPref(data['Rain']) as float, unit: settings.rainUnits) + child?.sendEvent(name: 'rainSumHour', value: rainToPref(data['sum_rain_1']) as float, unit: settings.rainUnits) + child?.sendEvent(name: 'rainSumDay', value: rainToPref(data['sum_rain_24']) as float, unit: settings.rainUnits) + child?.sendEvent(name: 'units', value: settings.rainUnits) + break; + case 'NAModule4': + log.debug "Updating NAModule4 $data" + child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale()) + child?.sendEvent(name: 'carbonDioxide', value: data['CO2']) + child?.sendEvent(name: 'humidity', value: data['Humidity']) + break; + } + } +} + +def cToPref(temp) { + if(getTemperatureScale() == 'C') { + return temp + } else { + return temp * 1.8 + 32 + } +} + +def rainToPref(rain) { + if(settings.rainUnits == 'mm') { + return rain + } else { + return rain * 0.039370 + } +} + +def debugEvent(message, displayEvent) { + + def results = [ + name: "appdebug", + descriptionText: message, + displayed: displayEvent + ] + log.debug "Generating AppDebug Event: ${results}" + sendEvent (results) + +} + +private Boolean canInstallLabs() { + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) { + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() { + return location.hubs*.firmwareVersionString.findAll { it } +} diff --git a/smartapps/dianoga/thermostat-auto-off.src/thermostat-auto-off.groovy b/smartapps/dianoga/thermostat-auto-off.src/thermostat-auto-off.groovy new file mode 100644 index 00000000000..fd06c6d703f --- /dev/null +++ b/smartapps/dianoga/thermostat-auto-off.src/thermostat-auto-off.groovy @@ -0,0 +1,83 @@ +/** + * HVAC Auto Off + * + * Author: dianoga7@3dgo.net + * Date: 2013-07-21 + */ + +// Automatically generated. Make future change here. +definition( + name: "Thermostat Auto Off", + namespace: "dianoga", + author: "dianoga7@3dgo.net", + description: "Automatically turn off thermostat when windows/doors open. Turn it back on when everything is closed up.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + oauth: true +) + +preferences { + section("Control") { + input("thermostat", "capability.thermostat", title: "Thermostat") + } + + section("Open/Close") { + input("sensors", "capability.contactSensor", title: "Sensors", multiple: true) + input("delay", "number", title: "Delay (seconds)") + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + unschedule() + initialize() +} + +def initialize() { + state.changed = false + subscribe(sensors, 'contact', "sensorChange") +} + +def sensorChange(evt) { + log.debug "Desc: $evt.value , $state" + if(evt.value == 'open' && !state.changed) { + unschedule() + runIn(delay, 'turnOff') + } else if(evt.value == 'closed' && state.changed) { + // All closed? + def isOpen = false + for(sensor in sensors) { + if(sensor.id != evt.deviceId && sensor.currentValue('contact') == 'open') { + isOpen = true + } + } + + if(!isOpen) { + unschedule() + runIn(delay, 'restore') + } + } +} + +def turnOff() { + log.debug "Turning off thermostat due to contact open" + state.thermostatMode = thermostat.currentValue("thermostatMode") + thermostat.off() + state.changed = true + log.debug "State: $state" +} + +def restore() { + log.debug "Setting thermostat to $state.thermostatMode" + thermostat.setThermostatMode(state.thermostatMode) + state.changed = false +} \ No newline at end of file diff --git a/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy b/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy new file mode 100644 index 00000000000..e7666ca4e7f --- /dev/null +++ b/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy @@ -0,0 +1,108 @@ +/** + * Whole House Fan + * + * Copyright 2014 Brian Steere + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Whole House Fan", + namespace: "dianoga", + author: "Brian Steere", + description: "Toggle a whole house fan (switch) when: Outside is cooler than inside, Inside is above x temp, Thermostat is off", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan%402x.png" +) + + +preferences { + section("Outdoor") { + input "outTemp", "capability.temperatureMeasurement", title: "Outdoor Thermometer", required: true + } + + section("Indoor") { + input "inTemp", "capability.temperatureMeasurement", title: "Indoor Thermometer", required: true + input "minTemp", "number", title: "Minimum Indoor Temperature" + input "fans", "capability.switch", title: "Vent Fan", multiple: true, required: true + } + + section("Thermostat") { + input "thermostat", "capability.thermostat", title: "Thermostat" + } + + section("Windows/Doors") { + paragraph "[Optional] Only turn on the fan if at least one of these is open" + input "checkContacts", "enum", title: "Check windows/doors", options: ['Yes', 'No'], required: true + input "contacts", "capability.contactSensor", title: "Windows/Doors", multiple: true, required: false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + state.fanRunning = false; + + subscribe(outTemp, "temperature", "checkThings"); + subscribe(inTemp, "temperature", "checkThings"); + subscribe(thermostat, "thermostatMode", "checkThings"); + subscribe(contacts, "contact", "checkThings"); +} + +def checkThings(evt) { + def outsideTemp = settings.outTemp.currentTemperature + def insideTemp = settings.inTemp.currentTemperature + def thermostatMode = settings.thermostat.currentThermostatMode + def somethingOpen = settings.checkContacts == 'No' || settings.contacts?.find { it.currentContact == 'open' } + + log.debug "Inside: $insideTemp, Outside: $outsideTemp, Thermostat: $thermostatMode, Something Open: $somethingOpen" + + def shouldRun = true; + + if(thermostatMode != 'off') { + log.debug "Not running due to thermostat mode" + shouldRun = false; + } + + if(insideTemp < outsideTemp) { + log.debug "Not running due to insideTemp > outdoorTemp" + shouldRun = false; + } + + if(insideTemp < settings.minTemp) { + log.debug "Not running due to insideTemp < minTemp" + shouldRun = false; + } + + if(!somethingOpen) { + log.debug "Not running due to nothing open" + shouldRun = false + } + + if(shouldRun && !state.fanRunning) { + fans.on(); + state.fanRunning = true; + } else if(!shouldRun && state.fanRunning) { + fans.off(); + state.fanRunning = false; + } +} \ No newline at end of file diff --git a/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy b/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy new file mode 100644 index 00000000000..26ffecaf4f2 --- /dev/null +++ b/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy @@ -0,0 +1,113 @@ +/** + * Its too humid! + * + * Copyright 2014 Brian Critchlow + * Based on Its too cold code by 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. + * + */ +definition( + name: "Humidity Alert!", + namespace: "docwisdom", + author: "Brian Critchlow", + description: "Notify me when the humidity rises above or falls below the given threshold. It will turn on a switch when it rises above the first threshold and off when it falls below the second threshold.", + category: "Convenience", + iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn", + iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn?displaySize=2x" +) + + +preferences { + section("Monitor the humidity of:") { + input "humiditySensor1", "capability.relativeHumidityMeasurement" + } + section("When the humidity rises above:") { + input "humidity1", "number", title: "Percentage ?" + } + section("When the humidity falls below:") { + input "humidity2", "number", title: "Percentage ?" + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "phone1", "phone", title: "Send a Text Message?", required: false + } + section("Control this switch:") { + input "switch1", "capability.switch", required: false + } +} + +def installed() { + subscribe(humiditySensor1, "humidity", humidityHandler) +} + +def updated() { + unsubscribe() + subscribe(humiditySensor1, "humidity", humidityHandler) +} + +def humidityHandler(evt) { + log.trace "humidity: ${evt.value}" + log.trace "set point: ${humidity1}" + + def currentHumidity = Double.parseDouble(evt.value.replace("%", "")) + def tooHumid = humidity1 + def notHumidEnough = humidity2 + def mySwitch = settings.switch1 + def deltaMinutes = 10 + + def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong()) + def recentEvents = humiditySensor1.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last ${deltaMinutes} minutes" + def alreadySentSms = recentEvents.count { Double.parseDouble(it.value.replace("%", "")) >= tooHumid } > 1 || recentEvents.count { Double.parseDouble(it.value.replace("%", "")) <= notHumidEnough } > 1 + + if (currentHumidity >= tooHumid) { + log.debug "Checking how long the humidity sensor has been reporting >= ${tooHumid}" + + // Don't send a continuous stream of text messages + + + + if (alreadySentSms) { + log.debug "Notification already sent within the last ${deltaMinutes} minutes" + + } else { + log.debug "Humidity Rose Above ${tooHumid}: sending SMS to $phone1 and activating ${mySwitch}" + send("${humiditySensor1.label} sensed high humidity level of ${evt.value}") + switch1?.on() + } + } + + if (currentHumidity <= notHumidEnough) { + log.debug "Checking how long the humidity sensor has been reporting <= ${notHumidEnough}" + + if (alreadySentSms) { + log.debug "Notification already sent within the last ${deltaMinutes} minutes" + + } else { + log.debug "Humidity Fell Below ${notHumidEnough}: sending SMS to $phone1 and activating ${mySwitch}" + send("${humiditySensor1.label} sensed high humidity level of ${evt.value}") + switch1?.off() + } + } +} + +private send(msg) { + if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + sendPush( msg ) + } + + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + + log.debug msg +} diff --git a/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy b/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy new file mode 100644 index 00000000000..73d5d46020e --- /dev/null +++ b/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy @@ -0,0 +1,49 @@ +/** + * Let There Be Dark! + * Turn your lights off when a Contact Sensor is opened and turn them back on when it is closed, ONLY if the Lights were previouly on. + * + * Author: SmartThings modified by Douglas Rich + */ +definition( + name: "Let There Be Dark!", + namespace: "Dooglave", + author: "Dooglave", + description: "Turn your lights off when a Contact Sensor is opened and turn them back on when it is closed, ONLY if the Lights were previouly on", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png" +) + +preferences { + section("When the door opens") { + input "contact1", "capability.contactSensor", title: "Where?" + } + section("Turn off a light") { + input "switch1", "capability.switch" + } +} + +def installed() { + subscribe(contact1, "contact", contactHandler) + subscribe(switch1, "switch.on", switchOnHandler) + subscribe(switch1, "switch.off", switchOffHandler) +} + +def updated() { + unsubscribe() + subscribe(contact1, "contact", contactHandler) + subscribe(switch1, "switch.on", switchOnHandler) + subscribe(switch1, "switch.off", switchOffHandler) +} + +def contactHandler(evt) { + log.debug "$evt.value" + if (evt.value == "open") { + state.wasOn = switch1.currentValue("switch") == "on" + switch1.off() +} + +if (evt.value == "closed") { + if(state.wasOn)switch1.on() +} +} \ No newline at end of file diff --git a/smartapps/egid/smart-windows.src/smart-windows.groovy b/smartapps/egid/smart-windows.src/smart-windows.groovy new file mode 100644 index 00000000000..89cdb040aa3 --- /dev/null +++ b/smartapps/egid/smart-windows.src/smart-windows.groovy @@ -0,0 +1,153 @@ +/** + * Smart Windows + * Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). + * + * Copyright 2014 Eric Gideon + * + * Based in part on the "When it's going to rain" SmartApp by the SmartThings team, + * primarily the message throttling code. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Smart Windows", + namespace: "egid", + author: "Eric Gideon", + description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.", + iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", + iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png" +) + + +preferences { + section( "Set the temperature range for your comfort zone..." ) { + input "minTemp", "number", title: "Minimum temperature" + input "maxTemp", "number", title: "Maximum temperature" + } + section( "Select windows to check..." ) { + input "sensors", "capability.contactSensor", multiple: true + } + section( "Select temperature devices to monitor..." ) { + input "inTemp", "capability.temperatureMeasurement", title: "Indoor" + input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false + } + section( "Set your location" ) { + input "zipCode", "text", title: "Zip code" + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "retryPeriod", "number", title: "Minutes between notifications:" + } +} + + +def installed() { + log.debug "Installed: $settings" + subscribe( inTemp, "temperature", temperatureHandler ) +} + +def updated() { + log.debug "Updated: $settings" + unsubscribe() + subscribe( inTemp, "temperature", temperatureHandler ) +} + + +def temperatureHandler(evt) { + def currentOutTemp = null + if ( outTemp ) { + currentOutTemp = outTemp.latestValue("temperature") + } else { + log.debug "No external temperature device set. Checking WUnderground...." + currentOutTemp = weatherCheck() + } + + def currentInTemp = evt.doubleValue + def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } + + log.trace "Temp event: $evt" + log.info "In: $currentInTemp; Out: $currentOutTemp" + + // Don't spam notifications + // *TODO* use state.foo from Severe Weather Alert to do this better + if (!retryPeriod) { + def retryPeriod = 30 + } + def timeAgo = new Date(now() - (1000 * 60 * retryPeriod).toLong()) + def recentEvents = inTemp.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriod minutes" + + // Figure out if we should notify + if ( currentInTemp > minTemp && currentInTemp < maxTemp ) { + log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp." + log.debug "No notifications sent." + } else if ( currentInTemp > maxTemp ) { + // Too warm. Can we do anything? + + def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1 + + if ( !alreadyNotified ) { + if ( currentOutTemp < maxTemp && !openWindows ) { + send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp > maxTemp && openWindows ) { + send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } else if ( currentInTemp < minTemp ) { + // Too cold! Is it warmer outside? + + def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1 + + if ( !alreadyNotified ) { + if ( currentOutTemp > minTemp && !openWindows ) { + send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp < minTemp && openWindows ) { + send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } +} + +def weatherCheck() { + def json = getWeatherFeature("conditions", zipCode) + def currentTemp = json?.current_observation?.temp_f + + if ( currentTemp ) { + log.trace "Temp: $currentTemp (WeatherUnderground)" + return currentTemp + } else { + log.warn "Did not get a temp: $json" + return false + } +} + +private send(msg) { + if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + sendPush( msg ) + sendEvent(linkText:app.label, descriptionText:msg, eventType:"SOLUTION_EVENT", displayed: true, name:"summary") + } + + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + + log.info msg +} \ No newline at end of file diff --git a/smartapps/egid/weather-windows.src/weather-windows.groovy b/smartapps/egid/weather-windows.src/weather-windows.groovy new file mode 100644 index 00000000000..7b7745e284f --- /dev/null +++ b/smartapps/egid/weather-windows.src/weather-windows.groovy @@ -0,0 +1,152 @@ +/** + * Weather Windows + * Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). + * + * Copyright 2015 Eric Gideon + * + * Based in part on the "When it's going to rain" SmartApp by the SmartThings team, + * primarily the message throttling code. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Weather Windows", + namespace: "egid", + author: "Eric Gideon", + category: "Convenience", + description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.", + iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", + iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png" +) + + +preferences { + section( "Set the temperature range for your comfort zone..." ) { + input "minTemp", "number", title: "Minimum temperature" + input "maxTemp", "number", title: "Maximum temperature" + } + section( "Select windows to check..." ) { + input "sensors", "capability.contactSensor", multiple: true + } + section( "Select temperature devices to monitor..." ) { + input "inTemp", "capability.temperatureMeasurement", title: "Indoor" + input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false + } + section( "Set your location" ) { + input "zipCode", "text", title: "Zip code" + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "retryPeriod", "number", title: "Minutes between notifications:" + } +} + + +def installed() { + log.debug "Installed: $settings" + subscribe( inTemp, "temperature", temperatureHandler ) +} + +def updated() { + log.debug "Updated: $settings" + unsubscribe() + subscribe( inTemp, "temperature", temperatureHandler ) +} + + +def temperatureHandler(evt) { + def currentOutTemp = null + if ( outTemp ) { + currentOutTemp = outTemp.latestValue("temperature") + } else { + log.debug "No external temperature device set. Checking WUnderground...." + currentOutTemp = weatherCheck() + } + + def currentInTemp = evt.doubleValue + def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } + + log.trace "Temp event: $evt" + log.info "In: $currentInTemp; Out: $currentOutTemp" + + // Don't spam notifications + // *TODO* use state.foo from Severe Weather Alert to do this better + def retryPeriodInMinutes = retryPeriod ?: 30 + def timeAgo = new Date(now() - (1000 * 60 * retryPeriodInMinutes).toLong()) + def recentEvents = inTemp.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriodInMinutes minutes" + + // Figure out if we should notify + if ( currentInTemp > minTemp && currentInTemp < maxTemp ) { + log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp." + log.debug "No notifications sent." + } else if ( currentInTemp > maxTemp ) { + // Too warm. Can we do anything? + + def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1 + + if ( !alreadyNotified ) { + if ( currentOutTemp < maxTemp && !openWindows ) { + send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp > maxTemp && openWindows ) { + send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } else if ( currentInTemp < minTemp ) { + // Too cold! Is it warmer outside? + + def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1 + + if ( !alreadyNotified ) { + if ( currentOutTemp > minTemp && !openWindows ) { + send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp < minTemp && openWindows ) { + send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } +} + +def weatherCheck() { + def json = getWeatherFeature("conditions", zipCode) + def currentTemp = json?.current_observation?.temp_f + + if ( currentTemp ) { + log.trace "Temp: $currentTemp (WeatherUnderground)" + return currentTemp + } else { + log.warn "Did not get a temp: $json" + return false + } +} + +private send(msg) { + if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + sendPush( msg ) + sendEvent(linkText:app.label, descriptionText:msg, eventType:"SOLUTION_EVENT", displayed: true, name:"summary") + } + + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + + log.info msg +} \ No newline at end of file diff --git a/smartapps/hwustrack/coffee-after-shower.src/coffee-after-shower.groovy b/smartapps/hwustrack/coffee-after-shower.src/coffee-after-shower.groovy new file mode 100644 index 00000000000..599759edc64 --- /dev/null +++ b/smartapps/hwustrack/coffee-after-shower.src/coffee-after-shower.groovy @@ -0,0 +1,55 @@ +/** + * Coffee After Shower + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Coffee After Shower", + namespace: "hwustrack", + author: "Hans Wustrack", + description: "This app is designed simply to turn on your coffee machine while you are taking a shower.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + + +preferences { + section("About") { + paragraph "This app is designed simply to turn on your coffee machine " + + "while you are taking a shower." + } + section("Bathroom humidity sensor") { + input "bathroom", "capability.relativeHumidityMeasurement", title: "Which humidity sensor?" + } + section("Coffee maker to turn on") { + input "coffee", "capability.switch", title: "Which switch?" + } + section("Humidity level to switch coffee on at") { + input "relHum", "number", title: "Humidity level?", defaultValue: 50 + } +} + +def installed() { + subscribe(bathroom, "humidity", coffeeMaker) +} + +def updated() { + unsubscribe() + subscribe(bathroom, "humidity", coffeeMaker) +} + +def coffeeMaker(shower) { + log.info "Humidity value: $shower.value" + if (shower.value.toInteger() > relHum) { + coffee.on() + } +} diff --git a/smartapps/imbrianj/door-knocker.src/door-knocker.groovy b/smartapps/imbrianj/door-knocker.src/door-knocker.groovy new file mode 100644 index 00000000000..53ca7d9d970 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/door-knocker.groovy @@ -0,0 +1,88 @@ +/** + * Door Knocker + * + * Author: brian@bevey.org + * Date: 9/10/13 + * + * Let me know when someone knocks on the door, but ignore + * when someone is opening the door. + */ + +definition( + name: "Door Knocker", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "Alert if door is knocked, but not opened.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("When Someone Knocks?") { + input name: "knockSensor", type: "capability.accelerationSensor", title: "Where?" + } + + section("But not when they open this door?") { + input name: "openSensor", type: "capability.contactSensor", title: "Where?" + } + + section("Knock Delay (defaults to 5s)?") { + input name: "knockDelay", type: "number", title: "How Long?", required: false + } + + section("Notifications") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } +} + +def installed() { + init() +} + +def updated() { + unsubscribe() + init() +} + +def init() { + state.lastClosed = 0 + subscribe(knockSensor, "acceleration.active", handleEvent) + subscribe(openSensor, "contact.closed", doorClosed) +} + +def doorClosed(evt) { + state.lastClosed = now() +} + +def doorKnock() { + if((openSensor.latestValue("contact") == "closed") && + (now() - (60 * 1000) > state.lastClosed)) { + log.debug("${knockSensor.label ?: knockSensor.name} detected a knock.") + send("${knockSensor.label ?: knockSensor.name} detected a knock.") + } + + else { + log.debug("${knockSensor.label ?: knockSensor.name} knocked, but looks like it was just someone opening the door.") + } +} + +def handleEvent(evt) { + def delay = knockDelay ?: 5 + runIn(delay, "doorKnock") +} + +private send(msg) { + if(sendPushMessage != "No") { + log.debug("Sending push message") + sendPush(msg) + } + + if(phone) { + log.debug("Sending text message") + sendSms(phone, msg) + } + + log.debug(msg) +} diff --git a/smartapps/imbrianj/forgiving-security.src/forgiving-security.groovy b/smartapps/imbrianj/forgiving-security.src/forgiving-security.groovy new file mode 100644 index 00000000000..b8630c1b224 --- /dev/null +++ b/smartapps/imbrianj/forgiving-security.src/forgiving-security.groovy @@ -0,0 +1,119 @@ +/** + * Forgiving Security + * + * Author: brian@bevey.org + * Date: 10/25/13 + * + * Arm a simple security system based on mode. Has a grace period to allow an + * ever present lag in presence detection. + */ + +definition( + name: "Forgiving Security", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "Alerts you if something happens while you're away. Has a settable grace period to compensate for presence sensors that may take a few seconds to be noticed.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("Things to secure?") { + input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false + input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false + } + + section("Alarms to go off?") { + input "alarms", "capability.alarm", title: "Which Alarms?", multiple: true, required: false + input "lights", "capability.switch", title: "Turn on which lights?", multiple: true, required: false + } + + section("Delay for presence lag?") { + input name: "presenceDelay", type: "number", title: "Seconds (defaults to 15s)", required: false + } + + section("Notifications?") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } + + section("Message interval?") { + input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false + } +} + +def installed() { + init() +} + +def updated() { + unsubscribe() + init() +} + +def init() { + state.lastTrigger = now() + state.deviceTriggers = [] + subscribe(contacts, "contact.open", triggerAlarm) + subscribe(motions, "motion.active", triggerAlarm) +} + +def triggerAlarm(evt) { + def presenceDelay = presenceDelay ?: 15 + + if(now() - (presenceDelay * 1000) > state.lastTrigger) { + log.warn("Stale event - ignoring") + + state.deviceTriggers = [] + } + + state.deviceTriggers.add(evt.displayName) + state.triggerMode = location.mode + state.lastTrigger = now() + + log.info(evt.displayName + " triggered an alarm. Waiting for presence lag.") + runIn(presenceDelay, "fireAlarm") +} + +def fireAlarm() { + if(state.deviceTriggers.size() > 0) { + def devices = state.deviceTriggers.unique().join(", ") + + if(location.mode == state.triggerMode) { + log.info(devices + " alarm triggered and mode hasn't changed.") + send(devices + " alarm has been triggered!") + lights?.on() + alarms?.both() + } + + else { + log.info(devices + " alarm triggered, but it looks like you were just coming home. Ignoring.") + } + } + + state.deviceTriggers = [] +} + +private send(msg) { + def delay = (messageDelay != null && messageDelay != "") ? messageDelay * 60 * 1000 : 0 + + if(now() - delay > state.lastMessage) { + state.lastMessage = now() + if(sendPushMessage == "Yes") { + log.debug("Sending push message.") + sendPush(msg) + } + + if(phone) { + log.debug("Sending text message.") + sendSms(phone, msg) + } + + log.debug(msg) + } + + else { + log.info("Have a message to send, but user requested to not get it.") + } +} diff --git a/smartapps/imbrianj/hall-light-welcome-home.src/hall-light-welcome-home.groovy b/smartapps/imbrianj/hall-light-welcome-home.src/hall-light-welcome-home.groovy new file mode 100644 index 00000000000..e6391873ea2 --- /dev/null +++ b/smartapps/imbrianj/hall-light-welcome-home.src/hall-light-welcome-home.groovy @@ -0,0 +1,77 @@ +/** + * Hall Light: Welcome Home + * + * Author: brian@bevey.org + * Date: 9/25/13 + * + * Turn on the hall light if someone comes home (presence) and the door opens. + */ + +definition( + name: "Hall Light: Welcome Home", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "Turn on the hall light if someone comes home (presence) and the door opens.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("People to watch for?") { + input "people", "capability.presenceSensor", multiple: true + } + + section("Front Door?") { + input "sensors", "capability.contactSensor", multiple: true + } + + section("Hall Light?") { + input "lights", "capability.switch", title: "Switch Turned On", multilple: true + } + + section("Presence Delay (defaults to 30s)?") { + input name: "presenceDelay", type: "number", title: "How Long?", required: false + } + + section("Door Contact Delay (defaults to 10s)?") { + input name: "contactDelay", type: "number", title: "How Long?", required: false + } +} + +def installed() { + init() +} + +def updated() { + unsubscribe() + init() +} + +def init() { + state.lastClosed = now() + subscribe(people, "presence.present", presence) + subscribe(sensors, "contact.open", doorOpened) +} + +def presence(evt) { + def delay = contactDelay ?: 10 + + state.lastPresence = now() + + if(now() - (delay * 1000) < state.lastContact) { + log.info('Presence was delayed, but you probably still want the light on.') + lights?.on() + } +} + +def doorOpened(evt) { + def delay = presenceDelay ?: 30 + + state.lastContact = now() + + if(now() - (delay * 1000) < state.lastPresence) { + log.info('Welcome home! Let me get that light for you.') + lights?.on() + } +} \ No newline at end of file diff --git a/smartapps/imbrianj/nobody-home.src/nobody-home.groovy b/smartapps/imbrianj/nobody-home.src/nobody-home.groovy new file mode 100644 index 00000000000..2588ea179d9 --- /dev/null +++ b/smartapps/imbrianj/nobody-home.src/nobody-home.groovy @@ -0,0 +1,164 @@ +/** + * Nobody Home + * + * Author: brian@bevey.org + * Date: 12/19/14 + * + * Monitors a set of presence detectors and triggers a mode change when everyone has left. + * When everyone has left, sets mode to a new defined mode. + * When at least one person returns home, set the mode back to a new defined mode. + * When someone is home - or upon entering the home, their mode may change dependent on sunrise / sunset. + */ + +definition( + name: "Nobody Home", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "When everyone leaves, change mode. If at least one person home, switch mode based on sun position.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("When all of these people leave home") { + input "people", "capability.presenceSensor", multiple: true + } + + section("Change to this mode to...") { + input "newAwayMode", "mode", title: "Everyone is away" + input "newSunsetMode", "mode", title: "At least one person home and nightfall" + input "newSunriseMode", "mode", title: "At least one person home and sunrise" + } + + section("Away threshold (defaults to 10 min)") { + input "awayThreshold", "decimal", title: "Number of minutes", required: false + } + + section("Notifications") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + } +} + +def installed() { + init() +} + +def updated() { + unsubscribe() + init() +} + +def init() { + subscribe(people, "presence", presence) + subscribe(location, "sunrise", setSunrise) + subscribe(location, "sunset", setSunset) + + state.sunMode = location.mode +} + +def setSunrise(evt) { + changeSunMode(newSunriseMode) +} + +def setSunset(evt) { + changeSunMode(newSunsetMode) +} + +def changeSunMode(newMode) { + state.sunMode = newMode + + if(everyoneIsAway() && (location.mode == newAwayMode)) { + log.debug("Mode is away, not evaluating") + } + + else if(location.mode != newMode) { + def message = "${app.label} changed your mode to '${newMode}'" + send(message) + setLocationMode(newMode) + } + + else { + log.debug("Mode is the same, not evaluating") + } +} + +def presence(evt) { + if(evt.value == "not present") { + log.debug("Checking if everyone is away") + + if(everyoneIsAway()) { + log.info("Starting ${newAwayMode} sequence") + def delay = (awayThreshold != null && awayThreshold != "") ? awayThreshold * 60 : 10 * 60 + runIn(delay, "setAway") + } + } + + else { + if(location.mode != state.sunMode) { + log.debug("Checking if anyone is home") + + if(anyoneIsHome()) { + log.info("Starting ${state.sunMode} sequence") + + changeSunMode(state.sunMode) + } + } + + else { + log.debug("Mode is the same, not evaluating") + } + } +} + +def setAway() { + if(everyoneIsAway()) { + if(location.mode != newAwayMode) { + def message = "${app.label} changed your mode to '${newAwayMode}' because everyone left home" + log.info(message) + send(message) + setLocationMode(newAwayMode) + } + + else { + log.debug("Mode is the same, not evaluating") + } + } + + else { + log.info("Somebody returned home before we set to '${newAwayMode}'") + } +} + +private everyoneIsAway() { + def result = true + + if(people.findAll { it?.currentPresence == "present" }) { + result = false + } + + log.debug("everyoneIsAway: ${result}") + + return result +} + +private anyoneIsHome() { + def result = false + + if(people.findAll { it?.currentPresence == "present" }) { + result = true + } + + log.debug("anyoneIsHome: ${result}") + + return result +} + +private send(msg) { + if(sendPushMessage != "No") { + log.debug("Sending push message") + sendPush(msg) + } + + log.debug(msg) +} diff --git a/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy new file mode 100644 index 00000000000..c88b91f6978 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy @@ -0,0 +1,134 @@ +/** + * Ready for Rain + * + * Author: brian@bevey.org + * Date: 9/10/13 + * + * Warn if doors or windows are open when inclement weather is approaching. + */ + +definition( + name: "Ready For Rain", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "Warn if doors or windows are open when inclement weather is approaching.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("Zip code?") { + input "zipcode", "text", title: "Zipcode?" + } + + section("Things to check?") { + input "sensors", "capability.contactSensor", multiple: true + } + + section("Notifications?") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } + + section("Message interval?") { + input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false + } +} + +def installed() { + init() +} + +def updated() { + unsubscribe() + unschedule() + init() +} + +def init() { + state.lastMessage = 0 + state.lastCheck = ["time": 0, "result": false] + schedule("0 0,30 * * * ?", scheduleCheck) // Check at top and half-past of every hour + subscribe(sensors, "contact.open", scheduleCheck) +} + +def scheduleCheck(evt) { + def open = sensors.findAll { it?.latestValue("contact") == "open" } + def plural = open.size() > 1 ? "are" : "is" + + // Only need to poll if we haven't checked in a while - and if something is left open. + if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) { + log.info("Something's open - let's check the weather.") + def response = getWeatherFeature("forecast", zipcode) + def weather = isStormy(response) + + if(weather) { + send("${open.join(', ')} ${plural} open and ${weather} coming.") + } + } + + else if(((now() - (30 * 60 * 1000) <= state.lastCheck["time"]) && state.lastCheck["result"]) && open) { + log.info("We have fresh weather data, no need to poll.") + send("${open.join(', ')} ${plural} open and ${state.lastCheck["result"]} coming.") + } + + else { + log.info("Everything looks closed, no reason to check weather.") + } +} + +private send(msg) { + def delay = (messageDelay != null && messageDelay != "") ? messageDelay * 60 * 1000 : 0 + + if(now() - delay > state.lastMessage) { + state.lastMessage = now() + if(sendPushMessage == "Yes") { + log.debug("Sending push message.") + sendPush(msg) + } + + if(phone) { + log.debug("Sending text message.") + sendSms(phone, msg) + } + + log.debug(msg) + } + + else { + log.info("Have a message to send, but user requested to not get it.") + } +} + +private isStormy(json) { + def types = ["rain", "snow", "showers", "sprinkles", "precipitation"] + def forecast = json?.forecast?.txt_forecast?.forecastday?.first() + def result = false + + if(forecast) { + def text = forecast?.fcttext?.toLowerCase() + + log.debug(text) + + if(text) { + for (int i = 0; i < types.size() && !result; i++) { + if(text.contains(types[i])) { + result = types[i] + } + } + } + + else { + log.warn("Got forecast, couldn't parse.") + } + } + + else { + log.warn("Did not get a forecast: ${json}") + } + + state.lastCheck = ["time": now(), "result": result] + + return result +} diff --git a/smartapps/imbrianj/safe-watch.src/safe-watch.groovy b/smartapps/imbrianj/safe-watch.src/safe-watch.groovy new file mode 100644 index 00000000000..13a272b5ad6 --- /dev/null +++ b/smartapps/imbrianj/safe-watch.src/safe-watch.groovy @@ -0,0 +1,131 @@ +/** + * Safe Watch + * + * Author: brian@bevey.org + * Date: 2013-11-17 + * + * Watch a series of sensors for any anomalies for securing a safe or room. + */ + +definition( + name: "Safe Watch", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "Watch a series of sensors for any anomalies for securing a safe.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("Things to secure?") { + input "contact", "capability.contactSensor", title: "Contact Sensor", required: false + input "motion", "capability.motionSensor", title: "Motion Sensor", required: false + input "knock", "capability.accelerationSensor", title: "Knock Sensor", required: false + input "axis", "capability.threeAxis", title: "Three-Axis Sensor", required: false + } + + section("Temperature monitor?") { + input "temp", "capability.temperatureMeasurement", title: "Temp Sensor", required: false + input "maxTemp", "number", title: "Max Temp?", required: false + input "minTemp", "number", title: "Min Temp?", required: false + } + + section("When which people are away?") { + input "people", "capability.presenceSensor", multiple: true + } + + section("Notifications?") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } + + section("Message interval?") { + input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false + } +} + +def installed() { + init() +} + +def updated() { + unsubscribe() + init() +} + +def init() { + subscribe(contact, "contact.open", triggerContact) + subscribe(motion, "motion.active", triggerMotion) + subscribe(knock, "acceleration.active", triggerKnock) + subscribe(temp, "temperature", triggerTemp) + subscribe(axis, "threeAxis", triggerAxis) +} + +def triggerContact(evt) { + if(everyoneIsAway()) { + send("Safe Watch: ${contact.label ?: contact.name} was opened!") + } +} + +def triggerMotion(evt) { + if(everyoneIsAway()) { + send("Safe Watch: ${motion.label ?: motion.name} sensed motion!") + } +} + +def triggerKnock(evt) { + if(everyoneIsAway()) { + send("Safe Watch: ${knock.label ?: knock.name} was knocked!") + } +} + +def triggerTemp(evt) { + def temperature = evt.doubleValue + + if((maxTemp && maxTemp < temperature) || + (minTemp && minTemp > temperature)) { + send("Safe Watch: ${temp.label ?: temp.name} is ${temperature}") + } +} + +def triggerAxis(evt) { + if(everyoneIsAway()) { + send("Safe Watch: ${axis.label ?: axis.name} was tilted!") + } +} + +private everyoneIsAway() { + def result = true + + if(people.findAll { it?.currentPresence == "present" }) { + result = false + } + + log.debug("everyoneIsAway: ${result}") + + return result +} + +private send(msg) { + def delay = (messageDelay != null && messageDelay != "") ? messageDelay * 60 * 1000 : 0 + + if(now() - delay > state.lastMessage) { + state.lastMessage = now() + if(sendPushMessage == "Yes") { + log.debug("Sending push message.") + sendPush(msg) + } + + if(phone) { + log.debug("Sending text message.") + sendSms(phone, msg) + } + + log.debug(msg) + } + + else { + log.info("Have a message to send, but user requested to not get it.") + } +} diff --git a/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy b/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy new file mode 100644 index 00000000000..7ae443d9b7c --- /dev/null +++ b/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy @@ -0,0 +1,128 @@ +/** + * Thermostat Window Check + * + * Author: brian@bevey.org + * Date: 9/13/13 + * + * If your heating or cooling system come on, it gives you notice if there are + * any windows or doors left open, preventing the system from working + * optimally. + */ + +definition( + name: "Thermostat Window Check", + namespace: "imbrianj", + author: "brian@bevey.org", + description: "If your heating or cooling system come on, it gives you notice if there are any windows or doors left open, preventing the system from working optimally.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("Things to check?") { + input "sensors", "capability.contactSensor", multiple: true + } + + section("Thermostats to monitor") { + input "thermostats", "capability.thermostat", multiple: true + } + + section("Notifications") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } + + section("Turn thermostat off automatically?") { + input "turnOffTherm", "enum", metadata: [values: ["Yes", "No"]], required: false + } + + section("Delay to wait before turning thermostat off (defaults to 1 minute)") { + input "turnOffDelay", "decimal", title: "Number of minutes", required: false + } +} + +def installed() { + subscribe(thermostats, "thermostatMode", thermoChange); + subscribe(sensors, "contact.open", windowChange); +} + +def updated() { + unsubscribe() + subscribe(thermostats, "thermostatMode", thermoChange); + subscribe(sensors, "contact.open", windowChange); +} + +def thermoChange(evt) { + if(evt.value == "heat" || + evt.value == "cool") { + def open = sensors.findAll { it?.latestValue("contact") == "open" } + + if(open) { + def plural = open.size() > 1 ? "are" : "is" + send("${open.join(', ')} ${plural} still open and the thermostat just came on.") + + thermoShutOffTrigger() + } + + else { + log.info("Thermostat came on and nothing is open."); + } + } +} + +def windowChange(evt) { + def heating = thermostats.findAll { it?.latestValue("thermostatMode") == "heat" } + def cooling = thermostats.findAll { it?.latestValue("thermostatMode") == "cool" } + + if(heating || cooling) { + def open = sensors.findAll { it?.latestValue("contact") == "open" } + def tempDirection = heating ? "heating" : "cooling" + def plural = open.size() > 1 ? "were" : "was" + send("${open.join(', ')} ${plural} opened and the thermostat is still ${tempDirection}.") + + thermoShutOffTrigger() + } +} + +def thermoShutOffTrigger() { + if(turnOffTherm == "Yes") { + log.info("Starting timer to turn off thermostat") + def delay = (turnOffDelay != null && turnOffDelay != "") ? turnOffDelay * 60 : 60 + state.turnOffTime = now() + + runIn(delay, "thermoShutOff") + } +} + +def thermoShutOff() { + def open = sensors.findAll { it?.latestValue("contact") == "open" } + def tempDirection = heating ? "heating" : "cooling" + def plural = open.size() > 1 ? "are" : "is" + + log.info("Checking if we need to turn thermostats off") + + if(open.size()) { + send("Thermostats turned off: ${open.join(', ')} ${plural} open and thermostats ${tempDirection}.") + log.info("Windows still open, turning thermostats off") + thermostats?.off() + } + + else { + log.info("Looks like everything is shut now - no need to turn off thermostats") + } +} + +private send(msg) { + if(sendPushMessage != "No") { + log.debug("Sending push message") + sendPush(msg) + } + + if(phone) { + log.debug("Sending text message") + sendSms(phone, msg) + } + + log.debug(msg) +} diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy new file mode 100644 index 00000000000..566f8e1e860 --- /dev/null +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -0,0 +1,305 @@ +/** + * Initial State Event Streamer + * + * Copyright 2015 David Sulpy + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Initial State Event Streamer", + namespace: "initialstate.events", + author: "David Sulpy", + description: "A SmartThings SmartApp to allow SmartThings events to be viewable inside an Initial State Event Bucket in your https://www.initialstate.com account.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertica_small.png", + iconX2Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png", + iconX3Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png", + oauth: [displayName: "Initial State", displayLink: "https://www.initialstate.com"]) + +import groovy.json.JsonSlurper + +preferences { + section("Choose which devices to monitor...") { + //input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false + input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false + //input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false + //input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false + //input "buttons", "capability.button", title: "Buttons", multiple: true, required: false + //input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false + //input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false + input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false + //input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false + //input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false + //input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false + input "locks", "capability.lock", title: "Locks", multiple: true, required: false + input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false + //input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false + //input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false + input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false + input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false + //input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false + //input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false + //input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false + //input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false + input "switches", "capability.switch", title: "Switches", multiple: true, required: false + input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false + input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false + //input "valves", "capability.valve", title: "Valves", multiple: true, required: false + input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false + } +} + +mappings { + path("/access_key") { + action: [ + GET: "getAccessKey", + PUT: "setAccessKey" + ] + } + path("/bucket") { + action: [ + GET: "getBucketKey", + PUT: "setBucketKey" + ] + } +} + +def subscribeToEvents() { + /*if (accelerometers != null) { + subscribe(accelerometers, "acceleration", genericHandler) + }*/ + if (alarms != null) { + subscribe(alarms, "alarm", genericHandler) + } + /*if (batteries != null) { + subscribe(batteries, "battery", genericHandler) + }*/ + /*if (beacons != null) { + subscribe(beacons, "presence", genericHandler) + }*/ + /* + if (buttons != null) { + subscribe(buttons, "button", genericHandler) + }*/ + /*if (cos != null) { + subscribe(cos, "carbonMonoxide", genericHandler) + }*/ + /*if (colors != null) { + subscribe(colors, "hue", genericHandler) + subscribe(colors, "saturation", genericHandler) + subscribe(colors, "color", genericHandler) + }*/ + if (contacts != null) { + subscribe(contacts, "contact", genericHandler) + } + /*if (doorsControllers != null) { + subscribe(doorsControllers, "door", genericHandler) + }*/ + /*if (energyMeters != null) { + subscribe(energyMeters, "energy", genericHandler) + }*/ + /*if (illuminances != null) { + subscribe(illuminances, "illuminance", genericHandler) + }*/ + if (locks != null) { + subscribe(locks, "lock", genericHandler) + } + if (motions != null) { + subscribe(motions, "motion", genericHandler) + } + /*if (musicPlayers != null) { + subscribe(musicPlayers, "status", genericHandler) + subscribe(musicPlayers, "level", genericHandler) + subscribe(musicPlayers, "trackDescription", genericHandler) + subscribe(musicPlayers, "trackData", genericHandler) + subscribe(musicPlayers, "mute", genericHandler) + }*/ + /*if (powerMeters != null) { + subscribe(powerMeters, "power", genericHandler) + }*/ + if (presences != null) { + subscribe(presences, "presence", genericHandler) + } + if (humidities != null) { + subscribe(humidities, "humidity", genericHandler) + } + /*if (relaySwitches != null) { + subscribe(relaySwitches, "switch", genericHandler) + }*/ + /*if (sleepSensors != null) { + subscribe(sleepSensors, "sleeping", genericHandler) + }*/ + /*if (smokeDetectors != null) { + subscribe(smokeDetectors, "smoke", genericHandler) + }*/ + /*if (peds != null) { + subscribe(peds, "steps", genericHandler) + subscribe(peds, "goal", genericHandler) + }*/ + if (switches != null) { + subscribe(switches, "switch", genericHandler) + } + if (switchLevels != null) { + subscribe(switchLevels, "level", genericHandler) + } + if (temperatures != null) { + subscribe(temperatures, "temperature", genericHandler) + } + if (thermostats != null) { + subscribe(thermostats, "temperature", genericHandler) + subscribe(thermostats, "heatingSetpoint", genericHandler) + subscribe(thermostats, "coolingSetpoint", genericHandler) + subscribe(thermostats, "thermostatSetpoint", genericHandler) + subscribe(thermostats, "thermostatMode", genericHandler) + subscribe(thermostats, "thermostatFanMode", genericHandler) + subscribe(thermostats, "thermostatOperatingState", genericHandler) + } + /*if (valves != null) { + subscribe(valves, "contact", genericHandler) + }*/ + if (waterSensors != null) { + subscribe(waterSensors, "water", genericHandler) + } +} + +def getAccessKey() { + log.trace "get access key" + if (state.accessKey == null) { + httpError(404, "Access Key Not Found") + } else { + [ + accessKey: state.accessKey + ] + } +} + +def getBucketKey() { + log.trace "get bucket key" + if (state.bucketKey == null) { + httpError(404, "Bucket key Not Found") + } else { + [ + bucketKey: state.bucketKey, + bucketName: state.bucketName + ] + } +} + +def setBucketKey() { + log.trace "set bucket key" + def newBucketKey = request.JSON?.bucketKey + def newBucketName = request.JSON?.bucketName + + log.debug "bucket name: $newBucketName" + log.debug "bucket key: $newBucketKey" + + if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) { + state.bucketKey = "$newBucketKey" + state.bucketName = "$newBucketName" + state.isBucketCreated = false + } +} + +def setAccessKey() { + log.trace "set access key" + def newAccessKey = request.JSON?.accessKey + + if (newAccessKey && newAccessKey != state.accessKey) { + state.accessKey = "$newAccessKey" + state.isBucketCreated = false + } +} + +def installed() { + + subscribeToEvents() + + state.isBucketCreated = false +} + +def updated() { + unsubscribe() + + if (state.bucketKey != null && state.accessKey != null) { + state.isBucketCreated = false + } + + subscribeToEvents() +} + +def createBucket() { + + if (!state.bucketName) { + state.bucketName = state.bucketKey + } + def bucketName = "${state.bucketName}" + def bucketKey = "${state.bucketKey}" + def accessKey = "${state.accessKey}" + + def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") + + def bucketCreatePost = [ + uri: 'https://groker.initialstate.com/api/buckets', + headers: [ + "Content-Type": "application/json", + "X-IS-AccessKey": accessKey + ], + body: bucketCreateBody + ] + + log.debug bucketCreatePost + + httpPostJson(bucketCreatePost) { + log.debug "bucket posted" + state.isBucketCreated = true + } +} + +def genericHandler(evt) { + log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value" + + def key = "$evt.displayName($evt.name)" + if (evt.unit != null) { + key = "$evt.displayName(${evt.name}_$evt.unit)" + } + def value = "$evt.value" + + eventHandler(key, value) +} + +def eventHandler(name, value) { + + if (state.accessKey == null || state.bucketKey == null) { + return + } + + if (!state.isBucketCreated) { + createBucket() + } + + def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]") + def eventPost = [ + uri: 'https://groker.initialstate.com/api/events', + headers: [ + "Content-Type": "application/json", + "X-IS-BucketKey": "${state.bucketKey}", + "X-IS-AccessKey": "${state.accessKey}" + ], + body: eventBody + ] + + log.debug eventPost + + httpPostJson(eventPost) { + log.debug "event data posted" + } +} \ No newline at end of file diff --git a/smartapps/jls/my-light-toggle.src/my-light-toggle.groovy b/smartapps/jls/my-light-toggle.src/my-light-toggle.groovy new file mode 100644 index 00000000000..569691391fa --- /dev/null +++ b/smartapps/jls/my-light-toggle.src/my-light-toggle.groovy @@ -0,0 +1,76 @@ +/** + * My Light Toggle + * + * Copyright 2015 Jesse Silverberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "My Light Toggle", + namespace: "JLS", + author: "Jesse Silverberg", + description: "Toggle lights on/off with a motion sensor", + category: "Convenience", + iconUrl: "https://www.dropbox.com/s/6kxtd2v5reggonq/lightswitch.gif?raw=1", + iconX2Url: "https://www.dropbox.com/s/6kxtd2v5reggonq/lightswitch.gif?raw=1", + iconX3Url: "https://www.dropbox.com/s/6kxtd2v5reggonq/lightswitch.gif?raw=1") + + +preferences { + section("When this sensor detects motion...") { + input "motionToggler", "capability.motionSensor", title: "Motion Here", required: true, multiple: false + } + + section("Master switch for the toggle reference...") { + input "masterToggle", "capability.switch", title: "Reference switch", required: true, multiple: false + } + + section("Toggle lights...") { + input "switchesToToggle", "capability.switch", title: "These go on/off", required: true, multiple: true + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(motionToggler, "motion", toggleSwitches) +} + + +def toggleSwitches(evt) { + log.debug "$evt.value" + + if (evt.value == "active" && masterToggle.currentSwitch == "off") { +// for (thisSwitch in switchesToToggle) { +// log.debug "$thisSwitch.label" +// thisSwitch.on() + switchesToToggle.on() + masterToggle.on() + } else if (evt.value == "active" && masterToggle.currentSwitch == "on") { +// for (thisSwitch in switchesToToggle) { +// log.debug "$thisSwitch.label" +// thisSwitch.off() + switchesToToggle.off() + masterToggle.off() + } + +} \ No newline at end of file diff --git a/smartapps/jonathan-a/auto-humidity-vent.src/auto-humidity-vent.groovy b/smartapps/jonathan-a/auto-humidity-vent.src/auto-humidity-vent.groovy new file mode 100644 index 00000000000..a0d45441eb4 --- /dev/null +++ b/smartapps/jonathan-a/auto-humidity-vent.src/auto-humidity-vent.groovy @@ -0,0 +1,277 @@ +/** + * Auto Humidity Vent + * + * Copyright 2014 Jonathan Andersson + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + + +definition ( + + name: "Auto Humidity Vent", + namespace: "jonathan-a", + author: "Jonathan Andersson", + description: "When the humidity reaches a specified level, activate one or more vent fans until the humidity is reduced to a specified level.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances11-icn.png", + iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances11-icn@2x.png" + +) + + +preferences { + + section("Enable / Disable the following functionality:") { + input "app_enabled", "bool", title: "Auto Humidity Vent", required:true, defaultValue:true + input "fan_control_enabled", "bool", title: "Vent Fan Control", required:true, defaultValue:true + } + + section("Choose a humidity sensor...") { + input "humidity_sensor", "capability.relativeHumidityMeasurement", title: "Humidity Sensor", required: true + } + section("Enter the relative humudity level (%) above which the vent fans will activate:") { + input "humidity_a", "number", title: "Humidity Activation Level", required: true, defaultValue:70 + } + section("Enter the relative humudity level (%) below which the vent fans will deactivate:") { + input "humidity_d", "number", title: "Humidity Deactivation Level", required: true, defaultValue:65 + } + + section("Select the vent fans to control...") { + input "fans", "capability.switch", title: "Vent Fans", multiple: true, required: true + } + + section("Select the vent fan energy meters to monitor...") { + input "emeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false + input "price_kwh", "decimal", title: "Cost in cents per kWh (12 is US avg)", required: true, defaultValue:12 + } + + section("Set notification options:") { + input "sendPushMessage", "bool", title: "Push notifications", required:true, defaultValue:false + input "phone", "phone", title: "Send text messages to", required: false + } + +} + + +def installed() { + + log.debug "${app.label} installed with settings: ${settings}" + + state.app_enabled = false + state.fan_control_enabled = false + + state.fansOn = false + state.fansOnTime = now() + state.fansLastRunTime = 0 + + initialize() + +} + + +def uninstalled() +{ + + send("${app.label} uninstalled.") + + state.app_enabled = false + + set_fans(false) + + state.fan_control_enabled = false + +} + + +def updated() { + + log.debug "${app.label} updated with settings: ${settings}" + + unsubscribe() + + initialize() + +} + + +def initialize() { + + if (settings.fan_control_enabled) { + if(state.fan_control_enabled == false) { + send("Vent Fan Control Enabled.") + } else { + log.debug "Vent Fan Control Enabled." + } + + state.fan_control_enabled = true + } else { + if(state.fan_control_enabled == true) { + send("Vent Fan Control Disabled.") + } else { + log.debug "Vent Fan Control Disabled." + } + + state.fan_control_enabled = false + } + + if (settings.app_enabled) { + if(state.app_enabled == false) { + send("${app.label} Enabled.") + } else { + log.debug "${app.label} Enabled." + } + + subscribe(humidity_sensor, "humidity", "handleThings") + + state.app_enabled = true + } else { + if(state.app_enabled == true) { + send("${app.label} Disabled.") + } else { + log.debug "${app.label} Disabled." + } + + state.app_enabled = false + } + + handleThings() + +} + + +def handleThings(evt) { + + + log.debug "handleThings()" + + if(evt) { + log.debug "$evt.descriptionText" + } + + def h = 0.0 as BigDecimal + if (settings.app_enabled) { + h = settings.humidity_sensor.currentValue('humidity') +/* + //Simulator is broken and requires this work around for testing. + if (settings.humidity_sensor.latestState('humidity')) { + log.debug settings.humidity_sensor.latestState('humidity').stringValue[0..-2] + h = settings.humidity_sensor.latestState('humidity').stringValue[0..-2].toBigDecimal() + } else { + h = 20 + } +*/ + } + + log.debug "Humidity: $h%, Activate: $humidity_a%, Deactivate: $humidity_d%" + + def activateFans = false + def deactivateFans = false + + if (settings.app_enabled) { + + if (state.fansOn) { + if (h > humidity_d) { + log.debug "Humidity not sufficient to deactivate vent fans: $h > $humidity_d" + } else { + log.debug "Humidity sufficient to deactivate vent fans: $h <= $humidity_d" + deactivateFans = true + } + } else { + if (h < humidity_a) { + log.debug "Humidity not sufficient to activate vent fans: $h < $humidity_a" + } else { + log.debug "Humidity sufficient to activate vent fans: $h >= $humidity_a" + activateFans = true + } + } + } + + if(activateFans) { + set_fans(true) + } + if(deactivateFans) { + set_fans(false) + } + +} + + +def set_fans(fan_state) { + + if (fan_state) { + if (state.fansOn == false) { + send("${app.label} fans On.") + state.fansOnTime = now() + if (settings.fan_control_enabled) { + if (emeters) { + emeters.reset() + } + fans.on() + } else { + send("${app.label} fan control is disabled.") + } + state.fansOn = true + } else { + log.debug "${app.label} fans already On." + } + } else { + if (state.fansOn == true) { + send("${app.label} fans Off.") + state.fansLastRunTime = (now() - state.fansOnTime) + + BigInteger ms = new java.math.BigInteger(state.fansLastRunTime) + int seconds = (BigInteger) (((BigInteger) ms / (1000I)) % 60I) + int minutes = (BigInteger) (((BigInteger) ms / (1000I * 60I)) % 60I) + int hours = (BigInteger) (((BigInteger) ms / (1000I * 60I * 60I)) % 24I) + int days = (BigInteger) ((BigInteger) ms / (1000I * 60I * 60I * 24I)) + + def sb = String.format("${app.label} cycle: %d:%02d:%02d:%02d", days, hours, minutes, seconds) + + send(sb) + + if (settings.fan_control_enabled) { + fans.off() + if (emeters) { + log.debug emeters.currentValue('energy') + //TODO: How to ensure latest (most accurate) energy reading? + emeters.poll() //[configure, refresh, on, off, poll, reset] +// emeters.refresh() //[configure, refresh, on, off, poll, reset] + state.fansLastRunEnergy = emeters.currentValue('energy').sum() + state.fansLastRunCost = ((state.fansLastRunEnergy * price_kwh) / 100.0) + send("${app.label} cycle: ${state.fansLastRunEnergy}kWh @ \$${state.fansLastRunCost}") + } + } else { + send("${app.label} fan control is disabled.") + } + state.fansOn = false + state.fansHoldoff = now() + } else { + log.debug "${app.label} fans already Off." + } + } + +} + + +private send(msg) { + + if (sendPushMessage) { + sendPush(msg) + } + + if (phone) { + sendSms(phone, msg) + } + + log.debug(msg) +} + diff --git a/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy b/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy new file mode 100644 index 00000000000..9ea4f3e4c06 --- /dev/null +++ b/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy @@ -0,0 +1,85 @@ +/** + * Jawbone Panic Button + * + * Copyright 2014 Juan Risso + * + * 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. + * + */ + +// Automatically generated. Make future change here. +definition( + name: "Jawbone Button Notifier", + namespace: "juano2310", + author: "Juan Risso", + category: "SmartThings Labs", + description: "Send push notifications or text messages with your Jawbone Up when you hold the button.", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png", +) + +preferences { + section("Use this Jawbone as a notification button and...") { + input "jawbone", "device.jawboneUser", multiple: true + } + section("Send a message when you press and hold the button...") { + input "warnMessage", "text", title: "Warning Message" + } + section("Or text message to these numbers (optional)") { + input ("phone1", "contact", required: false) { + input "phone1", "phone", required: false + } + input ("phone2", "contact", required: false) { + input "phone2", "phone", required: false + } + input ("phone3", "contact", required: false) { + input "phone3", "phone", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(jawbone, "sleeping", sendit) +} + +def sendit(evt) { + log.debug "$evt.value: $evt, $settings" + sendMessage() +} + +def sendMessage() { + log.debug "Sending Message" + def msg = warnMessage + log.info msg + if (phone1) { + sendSms phone1, msg + } + if (phone2) { + sendSms phone2, msg + } + if (phone3) { + sendSms phone3, msg + } + if (!phone1 && !phone2 && !phone3) { + sendPush msg + } +} diff --git a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy new file mode 100644 index 00000000000..7d9e193aa89 --- /dev/null +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -0,0 +1,502 @@ +/** + * Jawbone Service Manager + * + * Author: Juan Risso + * Date: 2013-12-19 + */ +definition( + name: "Jawbone UP (Connect)", + namespace: "juano2310", + author: "Juan Pablo Risso", + description: "Connect your Jawbone UP to SmartThings", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png", + oauth: true, + usePreferencesForAuthorization: false +) { + appSetting "clientId" + appSetting "clientSecret" + appSetting "serverUrl" +} + +preferences { + page(name: "Credentials", title: "Jawbone UP", content: "authPage", install: false) +} + +mappings { + path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } + path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } + path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } + path("/oauth/callback") { action: [ GET: "callback" ] } +} + +def getSmartThingsClientId() { + return appSettings.clientId +} + +def getSmartThingsClientSecret() { + return appSettings.clientSecret +} + +def callback() { + def redirectUrl = null + if (params.authQueryString) { + redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) + log.debug "redirectUrl: ${redirectUrl}" + } else { + log.warn "No authQueryString" + } + + if (state.JawboneAccessToken) { + log.debug "Access token already exists" + setup() + success() + } else { + def code = params.code + if (code) { + if (code.size() > 6) { + // Jawbone code + log.debug "Exchanging code for access token" + receiveToken(redirectUrl) + } else { + // SmartThings code, which we ignore, as we don't need to exchange for an access token. + // Instead, go initiate the Jawbone OAuth flow. + log.debug "Executing callback redirect to auth page" + def stcid = getSmartThingsClientId() + state.oauthInitState = UUID.randomUUID().toString() + def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"] + redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") + } + } else { + log.debug "This code should be unreachable" + success() + } + } +} + +def authPage() { + log.debug "authPage" + def description = null + if (state.JawboneAccessToken == null) { + if (!state.accessToken) { + log.debug "About to create access token" + createAccessToken() + } + description = "Click to enter Jawbone Credentials" + def redirectUrl = oauthInitUrl() + // log.debug "RedirectURL = ${redirectUrl}" + return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) { + section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description } + } + } else { + description = "Jawbone Credentials Already Entered." + return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) { + section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description } + } + } +} + +def oauthInitUrl() { + log.debug "oauthInitUrl" + def stcid = getSmartThingsClientId() + state.oauthInitState = UUID.randomUUID().toString() + def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ] + return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}" +} + +def receiveToken(redirectUrl = null) { + log.debug "receiveToken" + def stcid = getSmartThingsClientId() + def oauthClientSecret = getSmartThingsClientSecret() + def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ] + def params = [ + uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}", + ] + httpGet(params) { response -> + log.debug "${response.data}" + log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}" + state.JawboneAccessToken = response.data.access_token + state.refreshToken = response.data.refresh_token + } + + setup() + if (state.JawboneAccessToken) { + success() + } else { + def message = """ +

The connection could not be established!

+

Click 'Done' to return to the menu.

+ """ + connectionStatus(message) + } +} + +def success() { + def message = """ +

Your Jawbone Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

+ """ + connectionStatus(message) +} + +def receivedToken() { + def message = """ +

Your Jawbone Account is already connected to SmartThings!

+

Click 'Done' to finish setup.

+ """ + connectionStatus(message) +} + +def connectionStatus(message, redirectUrl = null) { + def redirectHtml = "" + if (redirectUrl) { + redirectHtml = """ + + """ + } + + def html = """ + + + + + SmartThings Connection + + ${redirectHtml} + + +
+ Jawbone UP icon + connected device icon + SmartThings logo + ${message} +
+ + + """ + render contentType: 'text/html', data: html +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" } + +def buildRedirectUrl(page) { + // log.debug "buildRedirectUrl" + // /api/token/:st_token/smartapps/installations/:id/something + return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" +} + +def validateCurrentToken() { + log.debug "validateCurrentToken" + def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken" + def requestBody = "secret=${getSmartThingsClientSecret()}" + + try { + httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> + if (response.status == 200) { + log.debug "${response.data}" + log.debug "Setting refresh token to ${response.data.data.refresh_token}" + state.refreshToken = response.data.data.refresh_token + } + } + } catch (groovyx.net.http.HttpResponseException e) { + if (e.statusCode == 401) { // token is expired + log.debug "Access token is expired" + if (state.refreshToken) { // if we have this we are okay + def stcid = getSmartThingsClientId() + def oauthClientSecret = getSmartThingsClientSecret() + def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken] + def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}" + def params = [ + uri: tokenUrl + ] + httpGet(params) { refreshResponse -> + def data = refreshResponse.data + log.debug "Status: ${refreshResponse.status}, data: ${data}" + if (data.error) { + if (data.error == "access_denied") { + // User has removed authorization (probably) + log.warn "Access denied, because: ${data.error_description}" + state.remove("JawboneAccessToken") + state.remove("refreshToken") + } + } else { + log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}" + state.JawboneAccessToken = data.access_token + state.refreshToken = data.refresh_token + } + } + } + } + } catch (java.net.SocketTimeoutException e) { + log.warn "Connection timed out, not much we can do here" + } +} + +def initialize() { + def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" + def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" + log.debug "Callback URL: $webhook" + httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) +} + +def setup() { + // make sure this is going to work + validateCurrentToken() + + if (state.JawboneAccessToken) { + def urlmember = "https://jawbone.com/nudge/api/users/@me/" + def member = null + httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + member = response.data.data + } + + if (member) { + state.member = member + def externalId = "${app.id}.${member.xid}" + + // find the appropriate child device based on my app id and the device network id + def deviceWrapper = getChildDevice("${externalId}") + + // invoke the generatePresenceEvent method on the child device + log.debug "Device $externalId: $deviceWrapper" + if (!deviceWrapper) { + def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true]) + if (childDevice) { + log.debug "Child Device Successfully Created" + generateInitialEvent (member, childDevice) + } + } + } + + initialize() + } +} + +def installed() { + enableCallback() + + if (!state.accessToken) { + log.debug "About to create access token" + createAccessToken() + } + + if (state.JawboneAccessToken) { + setup() + } +} + +def updated() { + enableCallback() + + if (!state.accessToken) { + log.debug "About to create access token" + createAccessToken() + } + + if (state.JawboneAccessToken) { + setup() + } +} + +def uninstalled() { + if (state.JawboneAccessToken) { + try { + httpDelete(uri: "https://jawbone.com/nudge/api/v.1.0/users/@me/PartnerAppMembership", headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) { response -> + log.debug "Success disconnecting Jawbone from SmartThings" + } + } catch (groovyx.net.http.HttpResponseException e) { + log.error "Error disconnecting Jawbone from SmartThings: ${e.statusCode}" + } + } +} + +def pollChild(childDevice) { + def member = state.member + generatePollingEvents (member, childDevice) +} + +def generatePollingEvents (member, childDevice) { + // lets figure out if the member is currently "home" (At the place) + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" + def goals = null + def moves = null + def sleeps = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + goals = response.data.data + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + moves = response.data.data.items[0] + } + + try { // we are going to just ignore any errors + log.debug "Member = ${member.first}" + log.debug "Moves Goal = ${goals.move_steps} Steps" + log.debug "Moves = ${moves.details.steps} Steps" + + childDevice?.sendEvent(name:"steps", value: moves.details.steps) + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + //setColor(moves.details.steps,goals.move_steps,childDevice) + } + catch (e) { + // eat it + } +} + +def generateInitialEvent (member, childDevice) { + // lets figure out if the member is currently "home" (At the place) + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" + def goals = null + def moves = null + def sleeps = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + goals = response.data.data + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + moves = response.data.data.items[0] + } + + try { // we are going to just ignore any errors + log.debug "Member = ${member.first}" + log.debug "Moves Goal = ${goals.move_steps} Steps" + log.debug "Moves = ${moves.details.steps} Steps" + log.debug "Sleeping state = false" + childDevice?.generateSleepingEvent(false) + childDevice?.sendEvent(name:"steps", value: moves.details.steps) + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + //setColor(moves.details.steps,goals.move_steps,childDevice) + } + catch (e) { + // eat it + } +} + +def setColor (steps,goal,childDevice) { + def result = steps * 100 / goal + if (result < 25) + childDevice?.sendEvent(name:"steps", value: "steps", label: steps) + else if ((result >= 25) && (result < 50)) + childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) + else if ((result >= 50) && (result < 75)) + childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) + else if (result >= 75) + childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) +} + +def hookEventHandler() { + // log.debug "In hookEventHandler method." + log.debug "request = ${request}" + + def json = request.JSON + + // get some stuff we need + def userId = json.events.user_xid[0] + def json_type = json.events.type[0] + def json_action = json.events.action[0] + + //log.debug json + log.debug "Userid = ${userId}" + log.debug "Notification Type: " + json_type + log.debug "Notification Action: " + json_action + + // find the appropriate child device based on my app id and the device network id + def externalId = "${app.id}.${userId}" + def childDevice = getChildDevice("${externalId}") + + if (childDevice) { + switch (json_action) { + case "enter_sleep_mode": + childDevice?.generateSleepingEvent(true) + break + case "exit_sleep_mode": + childDevice?.generateSleepingEvent(false) + break + case "creation": + childDevice?.sendEvent(name:"steps", value: 0) + break + case "updation": + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def goals = null + def moves = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + goals = response.data.data + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + moves = response.data.data.items[0] + } + log.debug "Goal = ${goals.move_steps} Steps" + log.debug "Steps = ${moves.details.steps} Steps" + childDevice?.sendEvent(name:"steps", value: moves.details.steps) + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + //setColor(moves.details.steps,goals.move_steps,childDevice) + break + case "deletion": + app.delete() + break + } + } + else { + log.debug "Couldn't find child device associated with Jawbone." + } + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html +} \ No newline at end of file diff --git a/smartapps/kristopherkubicki/turn-off-with-motion.src/turn-off-with-motion.groovy b/smartapps/kristopherkubicki/turn-off-with-motion.src/turn-off-with-motion.groovy new file mode 100644 index 00000000000..951db43902e --- /dev/null +++ b/smartapps/kristopherkubicki/turn-off-with-motion.src/turn-off-with-motion.groovy @@ -0,0 +1,82 @@ +/** + * Turn Off With Motion + * + * Copyright 2014 Kristopher Kubicki + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Turn Off With Motion", + namespace: "KristopherKubicki", + author: "Kristopher Kubicki", + description: "Turns off a device if there is motion", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + + + +preferences { + section("Turn off when there's movement..."){ + input "motion1", "capability.motionSensor", title: "Where?", multiple: true + } + section("And on when there's been no movement for..."){ + input "minutes1", "number", title: "Minutes?" + } + section("Turn off/on light(s)..."){ + input "switches", "capability.switch", multiple: true + } +} + + +def installed() +{ + subscribe(motion1, "motion", motionHandler) + schedule("0 * * * * ?", "scheduleCheck") +} + +def updated() +{ + unsubscribe() + subscribe(motion1, "motion", motionHandler) + unschedule() + schedule("0 * * * * ?", "scheduleCheck") +} + +def motionHandler(evt) { + log.debug "$evt.name: $evt.value" + + if (evt.value == "active") { + log.debug "turning on lights" + switches.off() + state.inactiveAt = null + } else if (evt.value == "inactive") { + if (!state.inactiveAt) { + state.inactiveAt = now() + } + } +} + +def scheduleCheck() { + log.debug "schedule check, ts = ${state.inactiveAt}" + if (state.inactiveAt) { + def elapsed = now() - state.inactiveAt + def threshold = 1000 * 60 * minutes1 + if (elapsed >= threshold) { + log.debug "turning off lights" + switches.on() + state.inactiveAt = null + } + else { + log.debug "${elapsed / 1000} sec since motion stopped" + } + } +} \ No newline at end of file diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy new file mode 100644 index 00000000000..f21c6fc8073 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy @@ -0,0 +1,96 @@ +definition( + name: "Enhanced Auto Lock Door", + namespace: "Lock Auto Super Enhanced", + author: "Arnaud", + description: "Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.", + category: "Safety & Security", + iconUrl: "http://www.gharexpert.com/mid/4142010105208.jpg", + iconX2Url: "http://www.gharexpert.com/mid/4142010105208.jpg" +) + +preferences{ + section("Select the door lock:") { + input "lock1", "capability.lock", required: true + } + section("Select the door contact sensor:") { + input "contact", "capability.contactSensor", required: true + } + section("Automatically lock the door when closed...") { + input "minutesLater", "number", title: "Delay (in minutes):", required: true + } + section("Automatically unlock the door when open...") { + input "secondsLater", "number", title: "Delay (in seconds):", required: true + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false + input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false + } +} + +def installed(){ + initialize() +} + +def updated(){ + unsubscribe() + unschedule() + initialize() +} + +def initialize(){ + log.debug "Settings: ${settings}" + subscribe(lock1, "lock", doorHandler, [filterEvents: false]) + subscribe(lock1, "unlock", doorHandler, [filterEvents: false]) + subscribe(contact, "contact.open", doorHandler) + subscribe(contact, "contact.closed", doorHandler) +} + +def lockDoor(){ + log.debug "Locking the door." + lock1.lock() + log.debug ( "Sending Push Notification..." ) + if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" ) + log.debug("Sending text message...") + if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" ) +} + +def unlockDoor(){ + log.debug "Unlocking the door." + lock1.unlock() + log.debug ( "Sending Push Notification..." ) + if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" ) + log.debug("Sending text message...") + if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" ) +} + +def doorHandler(evt){ + if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then... + def delay = (secondsLater) // runIn uses seconds + runIn( delay, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged. + } + else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then... + unschedule( unlockDoor ) // ...we don't need to unlock it later. + } + else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then... + unschedule( lockDoor ) // ...we don't need to lock it later. + } + else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then... + def delay = (minutesLater * 60) // runIn uses seconds + runIn( delay, lockDoor ) // ...schedule (in minutes) to lock. + } + else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door... + unschedule( lockDoor ) // ...we don't need to lock it later. + } + else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door... + def delay = (minutesLater * 60) // runIn uses seconds + runIn( delay, lockDoor ) // ...schedule (in minutes) to lock. + } + else { //Opening or Closing door when locked (in case you have a handle lock) + log.debug "Unlocking the door." + lock1.unlock() + log.debug ( "Sending Push Notification..." ) + if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" ) + log.debug("Sending text message...") + if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" ) + } +} \ No newline at end of file diff --git a/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy b/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy new file mode 100644 index 00000000000..8be8a7001c4 --- /dev/null +++ b/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy @@ -0,0 +1,53 @@ +/** + * + * Lights On When Door Open After Sundown + * + * Based on "Turn It On When It Opens" by SmartThings + * + * Author: Aaron Crocco + */ +preferences { + section("When the door opens..."){ + input "contact1", "capability.contactSensor", title: "Where?" + } + section("Turn on these lights..."){ + input "switches", "capability.switch", multiple: true + } + section("and change mode to...") { + input "HomeAfterDarkMode", "mode", title: "Mode?" + } +} + + +def installed() +{ + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + log.debug "$evt.value: $evt, $settings" + + //Check current time to see if it's after sundown. + def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) + def now = new Date() + def setTime = s.sunset + log.debug "Sunset is at $setTime. Current time is $now" + + + if (setTime.before(now)) { //Executes only if it's after sundown. + + log.trace "Turning on switches: $switches" + switches.on() + log.trace "Changing house mode to $HomeAfterDarkMode" + setLocationMode(HomeAfterDarkMode) + sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.") + + } +} + diff --git a/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy b/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy new file mode 100644 index 00000000000..620911b9f2b --- /dev/null +++ b/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy @@ -0,0 +1,111 @@ +/** + * Weather Underground PWS Connect + * + * Copyright 2015 Andrew Mager + * + * 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. + * + */ + +// This imports the Java class "DecimalFormat" +import java.text.DecimalFormat + +definition( + name: "Weather Underground PWS Connect", + namespace: "mager", + author: "Andrew Mager", + description: "Connect your SmartSense Temp/Humidity sensor to your Weather Underground Personal Weather Station.", + category: "Green Living", + iconUrl: "http://i.imgur.com/HU0ANBp.png", + iconX2Url: "http://i.imgur.com/HU0ANBp.png", + iconX3Url: "http://i.imgur.com/HU0ANBp.png", + oauth: true) + + +preferences { + section("Select a sensor") { + input "temp", "capability.temperatureMeasurement", title: "Temperature", required: true + input "humidity", "capability.relativeHumidityMeasurement", title: "Humidity", required: true + } + section("Configure your Weather Underground credentials") { + input "weatherID", "text", title: "Weather Station ID", required: true + input "password", "password", title: "Weather Underground password", required: true + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + + +def initialize() { + + /* + Check to see if the sensor is reporting temperature, then run the updateCurrentWeather + every 10 minutes + */ + if (temp.currentTemperature) { + runEvery5Minutes(updateCurrentWeather) + } +} + + +/* + Updates the Weather Underground Personal Weather Station (PWS) Upload Protocol + Reference: http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol +*/ +def updateCurrentWeather() { + + // Logs of the current data from the sensor + log.trace "Temp: " + temp.currentTemperature + log.trace "Humidity: " + humidity.currentHumidity + log.trace "Dew Point: " + calculateDewPoint(temp.currentTemperature, humidity.currentHumidity) + + // Builds the URL that will be sent to Weather Underground to update your PWS + def params = [ + uri: "http://weatherstation.wunderground.com", + path: "/weatherstation/updateweatherstation.php", + query: [ + "ID": weatherID, + "PASSWORD": password, + "dateutc": "now", + "tempf": temp.currentTemperature, + "humidity": humidity.currentHumidity, + "dewptf": calculateDewPoint(temp.currentTemperature, humidity.currentHumidity), + "action": "updateraw", + "softwaretype": "SmartThings" + ] + ] + + try { + // Make the HTTP request using httpGet() + httpGet(params) { resp -> // This is how we define the "return data". Can also use $it. + log.debug "response data: ${resp.data}" + } + } catch (e) { + log.error "something went wrong: $e" + } + +} + +// Calculates dewpoint based on temperature and humidity +def calculateDewPoint(t, rh) { + def dp = 243.04 * ( Math.log(rh / 100) + ( (17.625 * t) / (243.04 + t) ) ) / (17.625 - Math.log(rh / 100) - ( (17.625 * t) / (243.04 + t) ) ) + // Format the response for Weather Underground + return new DecimalFormat("##.##").format(dp) +} diff --git a/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy b/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy new file mode 100644 index 00000000000..f6be7eb4242 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy @@ -0,0 +1,132 @@ +/** + * Color Coordinator + * Version 1.0.0 - 7/4/15 + * By Michael Struck + * + * 1.0.0 - Initial release + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Color Coordinator", + namespace: "MichaelStruck", + author: "Michael Struck", + description: "Ties multiple colored lights to one specific light's settings", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC@2x.png" +) + +preferences { + page name: "mainPage" +} + +def mainPage() { + dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) { + section("Master Light") { + input "master", "capability.colorControl", title: "Colored Light" + } + section("Lights that follow the master settings") { + input "slaves", "capability.colorControl", title: "Colored Lights", multiple: true, required: false + } + section([mobileOnly:true], "Options") { + label(title: "Assign a name", required: false) + href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions" + } + } +} + +page(name: "pageAbout", title: "About ${textAppName()}") { + section { + paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" + } + section("Instructions") { + paragraph textHelp() + } +} + +def installed() { + init() +} + +def updated(){ + unsubscribe() + init() +} + +def init() { + subscribe(master, "switch", onOffHandler) + subscribe(master, "level", colorHandler) + subscribe(master, "hue", colorHandler) + subscribe(master, "saturation", colorHandler) + subscribe(master, "colorTemperature", tempHandler) +} +//----------------------------------- +def onOffHandler(evt){ + if (master.currentValue("switch") == "on"){ + slaves?.on() + } + else { + slaves?.off() + } +} + +def colorHandler(evt) { + def dimLevel = master.currentValue("level") + def hueLevel = master.currentValue("hue") + def saturationLevel = master.currentValue("saturation") + def newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer] + slaves?.setColor(newValue) +} + +def tempHandler(evt){ + if (evt.value != "--") { + def tempLevel = master.currentValue("colorTemperature") + slaves?.setColorTemperature(tempLevel) + } +} + +//Version/Copyright/Information/Help + +private def textAppName() { + def text = "Color Coordinator" +} + +private def textVersion() { + def text = "Version 1.0.0 (07/04/2015)" +} + +private def textCopyright() { + def text = "Copyright © 2015 Michael Struck" +} + +private def textLicense() { + def text = + "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"+ + "\n\n"+ + " http://www.apache.org/licenses/LICENSE-2.0"+ + "\n\n"+ + "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." +} + +private def textHelp() { + def text = + "This application will allow you to control the settings of multiple colored lights with one control. " + + "Simply choose a master control light, and then choose the lights that will follow the settings of the master, "+ + "including on/off conditions, hue, saturation, level and color temperature." +} \ No newline at end of file diff --git a/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy b/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy new file mode 100644 index 00000000000..071a2516d2a --- /dev/null +++ b/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy @@ -0,0 +1,419 @@ +/** + * Smart Home Ventilation + * Version 2.1.2 - 5/31/15 + * + * Copyright 2015 Michael Struck + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Smart Home Ventilation", + namespace: "MichaelStruck", + author: "Michael Struck", + description: "Allows for setting up various schedule scenarios for turning on and off home ventilation switches.", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent@2x.png", + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent@2x.png") + +preferences { + page name: "mainPage" +} + +def mainPage() { + dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) { + section("Select ventilation switches..."){ + input "switches", title: "Switches", "capability.switch", multiple: true + } + section ("Scheduling scenarios...") { + href(name: "toA_Scenario", page: "A_Scenario", title: getTitle (titleA, "A"), description: schedDesc(timeOnA1,timeOffA1,timeOnA2,timeOffA2,timeOnA3,timeOffA3,timeOnA4,timeOffA4, modeA, daysA), state: greyOut(timeOnA1,timeOnA2,timeOnA3,timeOnA4)) + href(name: "toB_Scenario", page: "B_Scenario", title: getTitle (titleB, "B"), description: schedDesc(timeOnB1,timeOffB1,timeOnB2,timeOffB2,timeOnB3,timeOffB3,timeOnB4,timeOffB4, modeB, daysB), state: greyOut(timeOnB1,timeOnB2,timeOnB3,timeOnB4)) + href(name: "toC_Scenario", page: "C_Scenario", title: getTitle (titleC, "C"), description: schedDesc(timeOnC1,timeOffC1,timeOnC2,timeOffC2,timeOnC3,timeOffC3,timeOnC4,timeOffC4, modeC, daysC), state: greyOut(timeOnC1,timeOnC2,timeOnC3,timeOnC4)) + href(name: "toD_Scenario", page: "D_Scenario", title: getTitle (titleD, "D"), description: schedDesc(timeOnD1,timeOffD1,timeOnD2,timeOffD2,timeOnD3,timeOffD3,timeOnD4,timeOffD4, modeD, daysD), state: greyOut(timeOnD1,timeOnD2,timeOnD3,timeOnD4)) + } + section([mobileOnly:true], "Options") { + label(title: "Assign a name", required: false, defaultValue: "Smart Home Ventilation") + href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions" + } + } +} +//----Scheduling Pages +page(name: "A_Scenario", title: getTitle (titleA, "A")) { + section{ + input "timeOnA1", title: "Schedule 1 time to turn on", "time", required: false + input "timeOffA1", title: "Schedule 1 time to turn off", "time", required: false + } + section{ + input "timeOnA2", title: "Schedule 2 time to turn on", "time", required: false + input "timeOffA2", title: "Schedule 2 time to turn off", "time", required: false + } + section{ + input "timeOnA3", title: "Schedule 3 time to turn on", "time", required: false + input "timeOffA3", title: "Schedule 3 time to turn off", "time", required: false + } + section{ + input "timeOnA4", title: "Schedule 4 time to turn on", "time", required: false + input "timeOffA4", title: "Schedule 4 time to turn off", "time", required: false + } + section ("Options") { + input "titleA", title: "Assign a scenario name", "text", required: false + input "modeA", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes" + input "daysA", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + } + } + +page(name: "B_Scenario", title: getTitle (titleB, "B")) { + section{ + input "timeOnB1", title: "Schedule 1 time to turn on", "time", required: false + input "timeOffB1", title: "Schedule 1 time to turn off", "time", required: false + } + section{ + input "timeOnB2", title: "Schedule 2 time to turn on", "time", required: false + input "timeOffB2", title: "Schedule 2 time to turn off", "time", required: false + } + section{ + input "timeOnB3", title: "Schedule 3 time to turn on", "time", required: false + input "timeOffB3", title: "Schedule 3 time to turn off", "time", required: false + } + section{ + input "timeOnB4", title: "Schedule 4 time to turn on", "time", required: false + input "timeOffB4", title: "Schedule 4 time to turn off", "time", required: false + } + section("Options") { + input "titleB", title: "Assign a scenario name", "text", required: false + input "modeB", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes" + input "daysB", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + } + } + +page(name: "C_Scenario", title: getTitle (titleC, "C")) { + section{ + input "timeOnC1", title: "Schedule 1 time to turn on", "time", required: false + input "timeOffC1", title: "Schedule 1 time to turn off", "time", required: false + } + section{ + input "timeOnC2", title: "Schedule 2 time to turn on", "time", required: false + input "timeOffC2", title: "Schedule 2 time to turn off", "time", required: false + } + section{ + input "timeOnC3", title: "Schedule 3 time to turn on", "time", required: false + input "timeOffC3", title: "Schedule 3 time to turn off", "time", required: false + } + section{ + input "timeOnC4", title: "Schedule 4 time to turn on", "time", required: false + input "timeOffC4", title: "Schedule 4 time to turn off", "time", required: false + } + section("Options") { + input "titleC", title: "Assign a scenario name", "text", required: false + input "modeC", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes" + input "daysC", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + } + } + + +page(name: "D_Scenario", title: getTitle (titleD, "D")) { + section{ + input "timeOnD1", title: "Schedule 1 time to turn on", "time", required: false + input "timeOffD1", title: "Schedule 1 time to turn off", "time", required: false + } + section{ + input "timeOnD2", title: "Schedule 2 time to turn on", "time", required: false + input "timeOffD2", title: "Schedule 2 time to turn off", "time", required: false + } + section{ + input "timeOnD3", title: "Schedule 3 time to turn on", "time", required: false + input "timeOffD3", title: "Schedule 3 time to turn off", "time", required: false + } + section{ + input "timeOnD4", title: "Schedule 4 time to turn on", "time", required: false + input "timeOffD4", title: "Schedule 4 time to turn off", "time", required: false + } + section("Options") { + input "titleD", title: "Assign a scenario name", "text", required: false + input "modeD", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes" + input "daysD", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + } + } + + +page(name: "pageAbout", title: "About ${textAppName()}") { + section { + paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" + } + section("Instructions") { + paragraph textHelp() + } +} + +// Install and initiate + +def installed() { + log.debug "Installed with settings: ${settings}" + init() +} + +def updated() { + unschedule() + turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule + unsubscribe + log.debug "Updated with settings: ${settings}" + init() +} + +def init() { + def midnightTime = timeToday("2000-01-01T00:01:00.999-0000", location.timeZone) + schedule (midnightTime, midNight) + subscribe(location, "mode", locationHandler) + startProcess() +} + +// Common methods + +def startProcess () { + createDayArray() + state.dayCount=state.data.size() + if (state.dayCount){ + state.counter = 0 + startDay() + } +} + +def startDay() { + def start = convertEpoch(state.data[state.counter].start) + def stop = convertEpoch(state.data[state.counter].stop) + + runOnce(start, turnOnSwitch, [overwrite: true]) + runOnce(stop, incDay, [overwrite: true]) +} + +def incDay() { + turnOffSwitch() + if (state.modeChange) { + startProcess() + } + else { + state.counter = state.counter + 1 + if (state.counter < state.dayCount) { + startDay() + } + } +} + +def locationHandler(evt) { + def result = false + state.modeChange = true + switches.each { + if (it.currentValue("switch")=="on"){ + result = true + } + } + if (!result) { + startProcess() + } +} + +def midNight(){ + startProcess() +} + +def turnOnSwitch() { + switches.on() + log.debug "Home ventilation switches are on." +} + +def turnOffSwitch() { + switches.each { + if (it.currentValue("switch")=="on"){ + it.off() + } + } + log.debug "Home ventilation switches are off." +} + +def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { + def title = "" + def dayListClean = "On " + def modeListClean ="Scenario runs in " + if (dayList && dayList.size() < 7) { + def dayListSize = dayList.size() + for (dayName in dayList) { + dayListClean = "${dayListClean}"+"${dayName}" + dayListSize = dayListSize -1 + if (dayListSize) { + dayListClean = "${dayListClean}, " + } + } + } + else { + dayListClean = "Every day" + } + if (modeList) { + def modeListSize = modeList.size() + def modePrefix ="modes" + if (modeListSize == 1) { + modePrefix = "mode" + } + for (modeName in modeList) { + modeListClean = "${modeListClean}"+"'${modeName}'" + modeListSize = modeListSize -1 + if (modeListSize) { + modeListClean = "${modeListClean}, " + } + else { + modeListClean = "${modeListClean} ${modePrefix}" + } + } + } + else { + modeListClean = "${modeListClean}all modes" + } + if (on1 && off1){ + title += "Schedule 1: ${humanReadableTime(on1)} to ${humanReadableTime(off1)}" + } + if (on2 && off2) { + title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}" + } + if (on3 && off3) { + title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}" + } + if (on4 && off4) { + title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}" + } + if (on1 || on2 || on3 || on4) { + title += "\n$modeListClean" + title += "\n$dayListClean" + } + + if (!on1 && !on2 && !on3 && !on4) { + title="Click to configure scenario" + } + title +} + +def greyOut(on1, on2, on3, on4){ + def result = on1 || on2 || on3 || on4 ? "complete" : "" +} + +public humanReadableTime(dateTxt) { + new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dateTxt).format("h:mm a", timeZone(dateTxt)) +} + +public convertEpoch(epochDate) { + new Date(epochDate).format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone) +} + +private getTitle(txt, scenario) { + def title = txt ? txt : "Scenario ${scenario}" +} + +private daysOk(dayList) { + def result = true + if (dayList) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = dayList.contains(day) + } + result +} + +private timeOk(starting, ending) { + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + if (start < stop && start >= currTime && stop>=currTime) { + state.data << [start:start, stop:stop] + } + } +} + +def createDayArray() { + state.modeChange = false + state.data = [] + if (modeA && modeA.contains(location.mode)) { + if (daysOk(daysA)){ + timeOk(timeOnA1, timeOffA1) + timeOk(timeOnA2, timeOffA2) + timeOk(timeOnA3, timeOffA3) + timeOk(timeOnA4, timeOffA4) + } + } + if (modeB && modeB.contains(location.mode)) { + if (daysOk(daysB)){ + timeOk(timeOnB1, timeOffB1) + timeOk(timeOnB2, timeOffB2) + timeOk(timeOnB3, timeOffB3) + timeOk(timeOnB4, timeOffB4) + } + } + if (modeC && modeC.contains(location.mode)) { + if (daysOk(daysC)){ + timeOk(timeOnC1, timeOffC1) + timeOk(timeOnC2, timeOffC2) + timeOk(timeOnC3, timeOffC3) + timeOk(timeOnC4, timeOffC4) + } + } + if (modeD && modeD.contains(location.mode)) { + if (daysOk(daysD)){ + timeOk(timeOnD1, timeOffD1) + timeOk(timeOnD2, timeOffD2) + timeOk(timeOnD3, timeOffD3) + timeOk(timeOnD4, timeOffD4) + } + } + state.data.sort{it.start} +} + +//Version/Copyright/Information/Help + +private def textAppName() { + def text = "Smart Home Ventilation" +} + +private def textVersion() { + def text = "Version 2.1.2 (05/31/2015)" +} + +private def textCopyright() { + def text = "Copyright © 2015 Michael Struck" +} + +private def textLicense() { + def text = + "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"+ + "\n\n"+ + " http://www.apache.org/licenses/LICENSE-2.0"+ + "\n\n"+ + "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." +} + +private def textHelp() { + def text = + "Within each scenario, choose a start and end time for the ventilation fan. You can have up to 4 different " + + "venting scenarios, and 4 schedules within each scenario. Each scenario can be restricted to specific modes or certain days of the week. It is recommended "+ + "that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " + + "avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " + + "at 12:15 am or later." +} \ No newline at end of file diff --git a/smartapps/michaelstruck/switch-activates-home-phrase-or-mode.src/switch-activates-home-phrase-or-mode.groovy b/smartapps/michaelstruck/switch-activates-home-phrase-or-mode.src/switch-activates-home-phrase-or-mode.groovy new file mode 100644 index 00000000000..6415c4fe28e --- /dev/null +++ b/smartapps/michaelstruck/switch-activates-home-phrase-or-mode.src/switch-activates-home-phrase-or-mode.groovy @@ -0,0 +1,146 @@ +/** + * Switch Activates Home Phrase or Mode + * + * Copyright 2015 Michael Struck + * Version 1.0.1 6/20/15 + * + * 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. + * + * Ties a Hello, Home phrase to a switch's (virtual or real) on/off state. Perfect for use with IFTTT. + * Simple define a switch to be used, then tie the on/off state of the switch to a specific Hello, Home phrases. + * Connect the switch to an IFTTT action, and the Hello, Home phrase will fire with the switch state change. + * + * + */ +definition( + name: "Switch Activates Home Phrase or Mode", + namespace: "MichaelStruck", + author: "Michael Struck", + description: "Ties a Hello, Home phrase or mode to a switch's state. Perfect for use with IFTTT.", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png", + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png") + + +preferences { + page(name: "getPref") +} + +def getPref() { + dynamicPage(name: "getPref", install:true, uninstall: true) { + section("Choose a switch to use...") { + input "controlSwitch", "capability.switch", title: "Switch", multiple: false, required: true + } + + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + section("Perform which phrase when...") { + input "phrase_on", "enum", title: "Switch is on", options: phrases, required: false + input "phrase_off", "enum", title: "Switch is off", options: phrases, required: false + } + } + section("Change to which mode when...") { + input "onMode", "mode", title: "Switch is on", required: false + input "offMode", "mode", title: "Switch is off", required: false + } + section([mobileOnly:true], "Options") { + label(title: "Assign a name", required: false) + mode title: "Set for specific mode(s)", required: false + href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions" + } + } +} + +page(name: "pageAbout", title: "About ${textAppName()}") { + section { + paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" + } + section("Instructions") { + paragraph textHelp() + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribe(controlSwitch, "switch", "switchHandler") +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribe(controlSwitch, "switch", "switchHandler") +} + +def switchHandler(evt) { + if (evt.value == "on" && (phrase_on || onMode)) { + if (phrase_on){ + location.helloHome.execute(settings.phrase_on) + } + if (onMode) { + changeMode(onMode) + } + } + else if (evt.value == "off" && (phrase_off || offMode)) { + if (phrase_off){ + location.helloHome.execute(settings.phrase_off) + } + if (offMode) { + changeMode(offMode) + } + } +} + +def changeMode(newMode) { + if (location.mode != newMode) { + if (location.modes?.find{it.name == newMode}) { + setLocationMode(newMode) + } else { + log.debug "Unable to change to undefined mode '${newMode}'" + } + } +} + +//Version/Copyright/Information/Help + +private def textAppName() { + def text = "Switch Activates Home Phrase or Mode" +} + +private def textVersion() { + def text = "Version 1.0.1 (06/20/2015)" +} + +private def textCopyright() { + def text = "Copyright © 2015 Michael Struck" +} + +private def textLicense() { + def text = + "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"+ + "\n\n"+ + " http://www.apache.org/licenses/LICENSE-2.0"+ + "\n\n"+ + "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." +} + +private def textHelp() { + def text = + "Ties a Hello, Home phrase or mode to a switch's (virtual or real) on/off state. Perfect for use with IFTTT. "+ + "Simple define a switch to be used, then tie the on/off state of the switch to a specific Hello, Home phrases or mode. "+ + "Connect the switch to an IFTTT action, and the Hello, Home phrase or mode will fire with the switch state change." +} \ No newline at end of file diff --git a/smartapps/michaelstruck/switch-activates-home-phrase.src/switch-activates-home-phrase.groovy b/smartapps/michaelstruck/switch-activates-home-phrase.src/switch-activates-home-phrase.groovy new file mode 100644 index 00000000000..a3a9c32025a --- /dev/null +++ b/smartapps/michaelstruck/switch-activates-home-phrase.src/switch-activates-home-phrase.groovy @@ -0,0 +1,72 @@ +/** + * Switch Activates Hello, Home Phrase + * + * Copyright 2015 Michael Struck + * Version 1.01 3/8/15 + * + * 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. + * + * Ties a Hello, Home phrase to a switch's (virtual or real) on/off state. Perfect for use with IFTTT. + * Simple define a switch to be used, then tie the on/off state of the switch to a specific Hello, Home phrases. + * Connect the switch to an IFTTT action, and the Hello, Home phrase will fire with the switch state change. + * + * + */ +definition( + name: "Switch Activates Home Phrase", + namespace: "MichaelStruck", + author: "Michael Struck", + description: "Ties a Hello, Home phrase to a switch's state. Perfect for use with IFTTT.", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png", + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png") + + +preferences { + page(name: "getPref") +} + +def getPref() { + dynamicPage(name: "getPref", title: "Choose Switch and Phrases", install:true, uninstall: true) { + section("Choose a switch to use...") { + input "controlSwitch", "capability.switch", title: "Switch", multiple: false, required: true + } + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + section("Perform the following phrase when...") { + log.trace phrases + input "phrase_on", "enum", title: "Switch is on", required: true, options: phrases + input "phrase_off", "enum", title: "Switch is off", required: true, options: phrases + } + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribe(controlSwitch, "switch", "switchHandler") +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribe(controlSwitch, "switch", "switchHandler") +} + +def switchHandler(evt) { + if (evt.value == "on") { + location.helloHome.execute(settings.phrase_on) + } else { + location.helloHome.execute(settings.phrase_off) + } +} + diff --git a/smartapps/michaelstruck/switch-changes-mode.src/switch-changes-mode.groovy b/smartapps/michaelstruck/switch-changes-mode.src/switch-changes-mode.groovy new file mode 100644 index 00000000000..84cb2e2851e --- /dev/null +++ b/smartapps/michaelstruck/switch-changes-mode.src/switch-changes-mode.groovy @@ -0,0 +1,76 @@ +/** + * Switch Changes Mode + * + * Copyright 2015 Michael Struck + * Version 1.01 3/8/15 + * + * 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. + * + * Ties a mode to a switch's (virtual or real) on/off state. Perfect for use with IFTTT. + * Simple define a switch to be used, then tie the on/off state of the switch to a specific mode. + * Connect the switch to an IFTTT action, and the mode will fire with the switch state change. + * + * + */ +definition( + name: "Switch Changes Mode", + namespace: "MichaelStruck", + author: "Michael Struck", + description: "Ties a mode to a switch's state. Perfect for use with IFTTT.", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png", + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png") + +preferences { + page(name: "getPref", title: "Choose Switch and Modes", install:true, uninstall: true) { + section("Choose a switch to use...") { + input "controlSwitch", "capability.switch", title: "Switch", multiple: false, required: true + } + section("Change to a new mode when...") { + input "onMode", "mode", title: "Switch is on", required: false + input "offMode", "mode", title: "Switch is off", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + subscribe(controlSwitch, "switch", "switchHandler") +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + subscribe(controlSwitch, "switch", "switchHandler") +} + +def switchHandler(evt) { + if (evt.value == "on") { + changeMode(onMode) + } else { + changeMode(offMode) + } +} + +def changeMode(newMode) { + + if (newMode && location.mode != newMode) { + if (location.modes?.find{it.name == newMode}) { + setLocationMode(newMode) + } + else { + log.debug "Unable to change to undefined mode '${newMode}'" + } + } +} + diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy new file mode 100644 index 00000000000..56fc46ab19e --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy @@ -0,0 +1,1340 @@ +/** + * Talking Alarm Clock + * + * Version - 1.0.0 5/23/15 + * Version - 1.1.0 5/24/15 - A song can now be selected to play after the voice greeting and bug fixes + * Version - 1.2.0 5/27/15 - Added About screen and misc code clean up and GUI revisions + * Version - 1.3.0 5/29/15 - Further code optimizations and addition of alarm summary action + * Version - 1.3.1 5/30/15 - Fixed one small code syntax issue in Scenario D + * Version - 1.4.0 6/7/15 - Revised About screen, enhanced the weather forecast voice summary, added a mode change option with alarm, and added secondary alarm options + * Version - 1.4.1 6/9/15 - Changed the mode change speech to make it clear when the mode change is taking place + * Version - 1.4.2 6/10/15 - To prevent accidental triggering of summary, put in a mode switch restriction + * Version - 1.4.3 6/12/15 - Syntax issues and minor GUI fixes + * Version - 1.4.4 6/15/15 - Fixed a bug with Phrase change at alarm time + * Version - 1.4.5 6/17/15 - Code optimization, implemented the new submitOnChange option and a new license agreement change + * + * Copyright 2015 Michael Struck - Uses code from Lighting Director by Tim Slagle & Michael Struck + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Talking Alarm Clock", + namespace: "MichaelStruck", + author: "Michael Struck", + description: "Control up to 4 waking schedules using a Sonos speaker as an alarm.", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock@2x.png", + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock@2x.png" + ) + +preferences { + page name:"pageMain" + page name:"pageSetupScenarioA" + page name:"pageSetupScenarioB" + page name:"pageSetupScenarioC" + page name:"pageSetupScenarioD" + page name:"pageWeatherSettingsA" //technically, these 4 pages should not be dynamic, but are here to work around a crash on the Andriod app + page name:"pageWeatherSettingsB" + page name:"pageWeatherSettingsC" + page name:"pageWeatherSettingsD" +} + +// Show setup page +def pageMain() { + dynamicPage(name: "pageMain", install: true, uninstall: true) { + section ("Alarms") { + href "pageSetupScenarioA", title: getTitle(ScenarioNameA, 1), description: getDesc(A_timeStart, A_sonos, A_day, A_mode), state: greyOut(ScenarioNameA, A_sonos, A_timeStart, A_alarmOn, A_alarmType) + if (ScenarioNameA && A_sonos && A_timeStart && A_alarmType){ + input "A_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "true", submitOnChange:true + } + } + section { + href "pageSetupScenarioB", title: getTitle(ScenarioNameB, 2), description: getDesc(B_timeStart, B_sonos, B_day, B_mode), state: greyOut(ScenarioNameB, B_sonos, B_timeStart, B_alarmOn, B_alarmType) + if (ScenarioNameB && B_sonos && B_timeStart && B_alarmType){ + input "B_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } + } + section { + href "pageSetupScenarioC", title: getTitle(ScenarioNameC, 3), description: getDesc(C_timeStart, C_sonos, C_day, C_mode), state: greyOut(ScenarioNameC, C_sonos, C_timeStart, C_alarmOn, C_alarmType) + if (ScenarioNameC && C_sonos && C_timeStart && C_alarmType){ + input "C_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } + } + section { + href "pageSetupScenarioD", title: getTitle(ScenarioNameD, 4), description: getDesc(D_timeStart, D_sonos, D_day, D_mode), state: greyOut(ScenarioNameD, D_sonos, D_timeStart, D_alarmOn, D_alarmType) + if (ScenarioNameD && D_sonos && D_timeStart && D_alarmType){ + input "D_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } + } + section([title:"Options", mobileOnly:true]) { + input "alarmSummary", "bool", title: "Enable Alarm Summary", defaultValue: "false", submitOnChange:true + if (alarmSummary) { + href "pageAlarmSummary", title: "Alarm Summary Settings", description: "Tap to configure alarm summary settings", state: "complete" + } + input "zipCode", "text", title: "Zip Code", required: false + label title:"Assign a name", required: false + href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions" + } + } +} + +page(name: "pageAlarmSummary", title: "Alarm Summary Settings") { + section { + input "summarySonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: false + input "summaryVolume", "number", title: "Set the summary volume", description: "0-100%", required: false + input "summaryDisabled", "bool", title: "Include disabled or unconfigured alarms in summary", defaultValue: "false" + input "summaryMode", "mode", title: "Speak summary only during the following modes...", multiple: true, required: false + } +} +//Show "pageSetupScenarioA" page +def pageSetupScenarioA() { + dynamicPage(name: "pageSetupScenarioA") { + section("Alarm settings") { + input "ScenarioNameA", "text", title: "Scenario Name", multiple: false, required: true + input "A_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + input "A_volume", "number", title: "Alarm volume", description: "0-100%", required: false + input "A_timeStart", "time", title: "Time to trigger alarm", required: true + input "A_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "A_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "A_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + + if (A_alarmType != "3") { + if (A_alarmType == "1"){ + input "A_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + } + if (A_alarmType == "2"){ + input "A_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + } + } + } + if (A_alarmType == "1"){ + section ("Alarm sound options"){ + input "A_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "A_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1")) { + section ("Voice greeting options") { + input "A_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsA", title: "Weather Reporting Settings", description: getWeatherDesc(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp), state: greyOut1(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp) + } + } + if (A_alarmType == "3" || (A_alarmType == "1" && A_secondAlarm =="2") || (A_alarmType == "2" && A_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "A_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(A_sonos, 1) + } + } + section("Devices to control in this alarm scenario") { + input "A_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersA", title: "Dimmer Settings", description: dimmerDesc(A_dimmers), state: greyOutOption(A_dimmers), submitOnChange:true + href "pageThermostatsA", title: "Thermostat Settings", description: thermostatDesc(A_thermostats, A_temperatureH, A_temperatureC), state: greyOutOption(A_thermostats), submitOnChange:true + if ((A_switches || A_dimmers || A_thermostats) && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + } + } + section ("Other actions at alarm time"){ + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + input "A_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (A_phrase && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } + } + input "A_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true + if (A_triggerMode && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + } + } + } +} + +page(name: "pageDimmersA", title: "Dimmer Settings") { + section { + input "A_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "A_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } +} + +page(name: "pageThermostatsA", title: "Thermostat Settings") { + section { + input "A_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } + section { + input "A_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" + input "A_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } +} + +def pageWeatherSettingsA() { + dynamicPage(name: "pageWeatherSettingsA", title: "Weather Reporting Settings") { + section { + input "A_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "A_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "A_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "A_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "A_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "A_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } +} + +//Show "pageSetupScenarioB" page +def pageSetupScenarioB() { + dynamicPage(name: "pageSetupScenarioB") { + section("Alarm settings") { + input "ScenarioNameB", "text", title: "Scenario Name", multiple: false, required: true + input "B_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + input "B_volume", "number", title: "Alarm volume", description: "0-100%", required: false + input "B_timeStart", "time", title: "Time to trigger alarm", required: true + input "B_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "B_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "B_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + + if (B_alarmType != "3") { + if (B_alarmType == "1"){ + input "B_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + } + if (B_alarmType == "2"){ + input "B_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + } + } + } + if (B_alarmType == "1"){ + section ("Alarm sound options"){ + input "B_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "B_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")){ + section ("Voice greeting options") { + input "B_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsB", title: "Weather Reporting Settings", description: getWeatherDesc(B_weatherReport, B_includeSunrise, B_includeSunset, B_includeTemp, B_humidity, B_localTemp), state: greyOut1(B_weatherReport, B_includeSunrise, B_includeSunset, B_includeTemp, B_humidity, B_localTemp) + } + } + if (B_alarmType == "3" || (B_alarmType == "1" && B_secondAlarm =="2") || (B_alarmType == "2" && B_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "B_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(B_sonos, 1) + } + } + section("Devices to control in this alarm scenario") { + input "B_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersB", title: "Dimmer Settings", description: dimmerDesc(B_dimmers), state: greyOutOption(B_dimmers), submitOnChange:true + href "pageThermostatsB", title: "Thermostat Settings", description: thermostatDesc(B_thermostats, B_temperatureH, B_temperatureC), state: greyOutOption(B_thermostats), submitOnChange:true + if ((B_switches || B_dimmers || B_thermostats) && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + } + } + section ("Other actions at alarm time"){ + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + input "B_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (B_phrase && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } + } + input "B_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true + if (B_triggerMode && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + } + } + } +} + +page(name: "pageDimmersB", title: "Dimmer Settings") { + section { + input "B_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "B_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } +} + +page(name: "pageThermostatsB", title: "Thermostat Settings") { + section { + input "B_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } + section { + input "B_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" + input "B_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } +} + +def pageWeatherSettingsB() { + dynamicPage(name: "pageWeatherSettingsB", title: "Weather Reporting Settings") { + section { + input "B_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "B_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "B_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "B_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "B_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "B_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } +} + +//Show "pageSetupScenarioC" page +def pageSetupScenarioC() { + dynamicPage(name: "pageSetupScenarioC") { + section("Alarm settings") { + input "ScenarioNameC", "text", title: "Scenario Name", multiple: false, required: true + input "C_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + input "C_volume", "number", title: "Alarm volume", description: "0-100%", required: false + input "C_timeStart", "time", title: "Time to trigger alarm", required: true + input "C_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "C_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "C_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + + if (C_alarmType != "3") { + if (C_alarmType == "1"){ + input "C_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + } + if (C_alarmType == "2"){ + input "C_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + } + } + } + if (C_alarmType == "1"){ + section ("Alarm sound options"){ + input "C_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "C_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + + if (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1")) { + section ("Voice greeting options") { + input "C_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsC", title: "Weather Reporting Settings", description: getWeatherDesc(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, A_humidity, C_localTemp), state: greyOut1(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, C_humidity, C_localTemp) } + } + + if (C_alarmType == "3" || (C_alarmType == "1" && C_secondAlarm =="2") || (C_alarmType == "2" && C_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "C_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(C_sonos, 1) + } + } + section("Devices to control in this alarm scenario") { + input "C_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersC", title: "Dimmer Settings", description: dimmerDesc(C_dimmers), state: greyOutOption(C_dimmers), submitOnChange:true + href "pageThermostatsC", title: "Thermostat Settings", description: thermostatDesc(C_thermostats, C_temperatureH, C_temperatureC), state: greyOutOption(C_thermostats), submitOnChange:true + if ((C_switches || C_dimmers || C_thermostats) && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + } + } + section ("Other actions at alarm time"){ + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + input "C_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (C_phrase && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } + } + input "C_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true + if (C_triggerMode && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + } + } + } +} + +page(name: "pageDimmersC", title: "Dimmer Settings") { + section { + input "C_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "C_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } +} + +page(name: "pageThermostatsC", title: "Thermostat Settings") { + section { + input "C_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } + section { + input "C_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" + input "C_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } +} + +def pageWeatherSettingsC() { + dynamicPage(name: "pageWeatherSettingsC", title: "Weather Reporting Settings") { + section { + input "C_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "C_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "C_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "C_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "C_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "C_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } +} + +//Show "pageSetupScenarioD" page +def pageSetupScenarioD() { + dynamicPage(name: "pageSetupScenarioD") { + section("Alarm settings") { + input "ScenarioNameD", "text", title: "Scenario Name", multiple: false, required: true + input "D_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + input "D_volume", "number", title: "Alarm volume", description: "0-100%", required: false + input "D_timeStart", "time", title: "Time to trigger alarm", required: true + input "D_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "D_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "D_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + + if (D_alarmType != "3") { + if (D_alarmType == "1"){ + input "D_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + } + if (D_alarmType == "2"){ + input "D_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + } + } + } + if (D_alarmType == "1"){ + section ("Alarm sound options"){ + input "D_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "D_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + + if (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1")) { + section ("Voice greeting options") { + input "D_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsD", title: "Weather Reporting Settings", description: getWeatherDesc(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp), state: greyOut1(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp) } + } + + if (D_alarmType == "3" || (D_alarmType == "1" && D_secondAlarm =="2") || (D_alarmType == "2" && D_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "D_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(D_sonos, 1) + } + } + section("Devices to control in this alarm scenario") { + input "D_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersD", title: "Dimmer Settings", description: dimmerDesc(D_dimmers), state: greyOutOption(D_dimmers), submitOnChange:true + href "pageThermostatsD", title: "Thermostat Settings", description: thermostatDesc(D_thermostats, D_temperatureH, D_temperatureC), state: greyOutOption(D_thermostats), submitOnChange:true + if ((D_switches || D_dimmers || D_thermostats) && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + } + } + section ("Other actions at alarm time"){ + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + input "D_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (D_phrase && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } + } + input "D_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true + if (D_triggerMode && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + } + } + } +} + +page(name: "pageDimmersD", title: "Dimmer Settings") { + section { + input "D_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "D_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } +} + +page(name: "pageThermostatsD", title: "Thermostat Settings") { + section { + input "D_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } + section { + input "D_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" + input "D_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } +} + +def pageWeatherSettingsD() { + dynamicPage(name: "pageWeatherSettingsD", title: "Weather Reporting Settings") { + section { + input "D_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "D_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "D_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "D_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "D_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "D_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } +} + +page(name: "pageAbout", title: "About ${textAppName()}") { + section { + paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" + } + section("Instructions") { + paragraph textHelp() + } +} + +//-------------------------------------- + +def installed() { + initialize() +} + +def updated() { + unschedule() + unsubscribe() + initialize() +} + +def initialize() { + if (A_alarmType =="1"){ + alarmSoundUri(A_soundAlarm, A_soundLength, 1) + } + if (B_alarmType =="1"){ + alarmSoundUri(B_soundAlarm, B_soundLength, 2) + } + if (C_alarmType =="1"){ + alarmSoundUri(C_soundAlarm, C_soundLength, 3) + } + if (D_alarmType =="1"){ + alarmSoundUri(D_soundAlarm, D_soundLength, 4) + } + + if (alarmSummary && summarySonos) { + subscribe(app, appTouchHandler) + } + if (ScenarioNameA && A_timeStart && A_sonos && A_alarmOn && A_alarmType){ + schedule (A_timeStart, alarm_A) + if (A_musicTrack){ + saveSelectedSong(A_sonos, A_musicTrack, 1) + } + } + if (ScenarioNameB && B_timeStart && B_sonos &&B_alarmOn && B_alarmType){ + schedule (B_timeStart, alarm_B) + if (B_musicTrack){ + saveSelectedSong(B_sonos, B_musicTrack, 2) + } + } + if (ScenarioNameC && C_timeStart && C_sonos && C_alarmOn && C_alarmType){ + schedule (C_timeStart, alarm_C) + if (C_musicTrack){ + saveSelectedSong(C_sonos, C_musicTrack, 3) + } + } + if (ScenarioNameD && D_timeStart && D_sonos && D_alarmOn && D_alarmType){ + schedule (D_timeStart, alarm_D) + if (D_musicTrack){ + saveSelectedSong(D_sonos, D_musicTrack, 4) + } + } +} + +//-------------------------------------- + +def alarm_A() { + if ((!A_mode || A_mode.contains(location.mode)) && getDayOk(A_day)) { + if (A_switches || A_dimmers || A_thermostats) { + def dimLevel = A_level as Integer + A_switches?.on() + A_dimmers?.setLevel(dimLevel) + if (A_thermostats) { + def thermostatState = A_thermostats.currentThermostatMode + if (thermostatState == "auto") { + A_thermostats.setHeatingSetpoint(A_temperatureH) + A_thermostats.setCoolingSetpoint(A_temperatureC) + } + else if (thermostatState == "heat") { + A_thermostats.setHeatingSetpoint(A_temperatureH) + log.info "Set $A_thermostats Heat $A_temperatureH°" + } + else { + A_thermostats.setCoolingSetpoint(A_temperatureC) + log.info "Set $A_thermostats Cool $A_temperatureC°" + } + } + } + if (A_phrase) { + location.helloHome.execute(A_phrase) + } + + if (A_triggerMode && location.mode != A_triggerMode) { + if (location.modes?.find{it.name == A_triggerMode}) { + setLocationMode(A_triggerMode) + } + else { + log.debug "Unable to change to undefined mode '${A_triggerMode}'" + } + } + + if (A_volume) { + A_sonos.setLevel(A_volume) + } + + if (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1")) { + state.fullMsgA = "" + if (A_wakeMsg) { + getGreeting(A_wakeMsg, 1) + } + + if (A_weatherReport || A_humidity || A_includeTemp || A_localTemp) { + getWeatherReport(1, A_weatherReport, A_humidity, A_includeTemp, A_localTemp) + } + + if (A_includeSunrise || A_includeSunset) { + getSunriseSunset(1, A_includeSunrise, A_includeSunset) + } + + if ((A_switches || A_dimmers || A_thermostats) && A_confirmSwitches) { + getOnConfimation(A_switches, A_dimmers, A_thermostats, 1) + } + + if (A_phrase && A_confirmPhrase) { + getPhraseConfirmation(1, A_phrase) + } + + if (A_triggerMode && A_confirmMode){ + getModeConfirmation(A_triggerMode, 1) + } + + state.soundA = textToSpeech(state.fullMsgA, true) + } + + if (A_alarmType == "1"){ + if (A_secondAlarm == "1" && state.soundAlarmA){ + A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.soundA.uri) + } + if (A_secondAlarm == "2" && state.selectedSongA && state.soundAlarmA){ + A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.selectedSongA) + } + if (!A_secondAlarm){ + A_sonos.playTrack(state.soundAlarmA.uri) + } + } + + if (A_alarmType == "2") { + if (A_secondAlarmMusic && state.selectedSongA){ + A_sonos.playSoundAndTrack (state.soundA.uri, state.soundA.duration, state.selectedSongA) + } + else { + A_sonos.playTrack(state.soundA.uri) + } + } + + if (A_alarmType == "3") { + A_sonos.playTrack(state.selectedSongA) + } + } +} + +def alarm_B() { + if ((!B_mode || B_mode.contains(location.mode)) && getDayOk(B_day)) { + if (B_switches || B_dimmers || B_thermostats) { + def dimLevel = B_level as Integer + B_switches?.on() + B_dimmers?.setLevel(dimLevel) + if (B_thermostats) { + def thermostatState = B_thermostats.currentThermostatMode + if (thermostatState == "auto") { + B_thermostats.setHeatingSetpoint(B_temperatureH) + B_thermostats.setCoolingSetpoint(B_temperatureC) + } + else if (thermostatState == "heat") { + B_thermostats.setHeatingSetpoint(B_temperatureH) + log.info "Set $B_thermostats Heat $B_temperatureH°" + } + else { + B_thermostats.setCoolingSetpoint(B_temperatureC) + log.info "Set $B_thermostats Cool $B_temperatureC°" + } + } + } + if (B_phrase) { + location.helloHome.execute(B_phrase) + } + + if (B_triggerMode && location.mode != B_triggerMode) { + if (location.modes?.find{it.name == B_triggerMode}) { + setLocationMode(B_triggerMode) + } + else { + log.debug "Unable to change to undefined mode '${B_triggerMode}'" + } + } + + if (B_volume) { + B_sonos.setLevel(B_volume) + } + + if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")) { + state.fullMsgB = "" + if (B_wakeMsg) { + getGreeting(B_wakeMsg, 2) + } + + if (B_weatherReport || B_humidity || B_includeTemp || B_localTemp) { + getWeatherReport(2, B_weatherReport, B_humidity, B_includeTemp, B_localTemp) + } + + if (B_includeSunrise || B_includeSunset) { + getSunriseSunset(2, B_includeSunrise, B_includeSunset) + } + + if ((B_switches || B_dimmers || B_thermostats) && B_confirmSwitches) { + getOnConfimation(B_switches, B_dimmers, B_thermostats, 2) + } + + if (B_phrase && B_confirmPhrase) { + getPhraseConfirmation(2, B_phrase) + } + + if (B_triggerMode && B_confirmMode){ + getModeConfirmation(B_triggerMode, 2) + } + + state.soundB = textToSpeech(state.fullMsgB, true) + } + + if (B_alarmType == "1"){ + if (B_secondAlarm == "1" && state.soundAlarmB) { + B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.soundB.uri) + } + if (B_secondAlarm == "2" && state.selectedSongB && state.soundAlarmB){ + B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.selectedSongB) + } + if (!B_secondAlarm){ + B_sonos.playTrack(state.soundAlarmB.uri) + } + } + + if (B_alarmType == "2") { + if (B_secondAlarmMusic && state.selectedSongB){ + B_sonos.playSoundAndTrack (state.soundB.uri, state.soundB.duration, state.selectedSongB) + } + else { + B_sonos.playTrack(state.soundB.uri) + } + } + + if (B_alarmType == "3") { + B_sonos.playTrack(state.selectedSongB) + } + } +} + +def alarm_C() { + if ((!C_mode || C_mode.contains(location.mode)) && getDayOk(C_day)) { + if (C_switches || C_dimmers || C_thermostats) { + def dimLevel = C_level as Integer + C_switches?.on() + C_dimmers?.setLevel(dimLevel) + if (C_thermostats) { + def thermostatState = C_thermostats.currentThermostatMode + if (thermostatState == "auto") { + C_thermostats.setHeatingSetpoint(C_temperatureH) + C_thermostats.setCoolingSetpoint(C_temperatureC) + } + else if (thermostatState == "heat") { + C_thermostats.setHeatingSetpoint(C_temperatureH) + log.info "Set $C_thermostats Heat $C_temperatureH°" + } + else { + C_thermostats.setCoolingSetpoint(C_temperatureC) + log.info "Set $C_thermostats Cool $C_temperatureC°" + } + } + } + if (C_phrase) { + location.helloHome.execute(C_phrase) + } + + if (C_triggerMode && location.mode != C_triggerMode) { + if (location.modes?.find{it.name == C_triggerMode}) { + setLocationMode(C_triggerMode) + } + else { + log.debug "Unable to change to undefined mode '${C_triggerMode}'" + } + } + + if (C_volume) { + C_sonos.setLevel(C_volume) + } + + if (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1")) { + state.fullMsgC = "" + if (C_wakeMsg) { + getGreeting(C_wakeMsg, 3) + } + + if (C_weatherReport || C_humidity || C_includeTemp || C_localTemp) { + getWeatherReport(3, C_weatherReport, C_humidity, C_includeTemp, C_localTemp) + } + + if (C_includeSunrise || C_includeSunset) { + getSunriseSunset(3, C_includeSunrise, C_includeSunset) + } + + if ((C_switches || C_dimmers || C_thermostats) && C_confirmSwitches) { + getOnConfimation(C_switches, C_dimmers, C_thermostats, 3) + } + + if (C_phrase && C_confirmPhrase) { + getPhraseConfirmation(3, C_phrase) + } + + if (C_triggerMode && C_confirmMode){ + getModeConfirmation(C_triggerMode, 3) + } + + state.soundC = textToSpeech(state.fullMsgC, true) + } + + if (C_alarmType == "1"){ + if (C_secondAlarm == "1" && state.soundAlarmC){ + C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.soundC.uri) + } + if (C_secondAlarm == "2" && state.selectedSongC && state.soundAlarmC){ + C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.selectedSongC) + } + if (!C_secondAlarm){ + C_sonos.playTrack(state.soundAlarmC.uri) + } + } + + if (C_alarmType == "2") { + if (C_secondAlarmMusic && state.selectedSongC){ + C_sonos.playSoundAndTrack (state.soundC.uri, state.soundC.duration, state.selectedSongC) + } + else { + C_sonos.playTrack(state.soundC.uri) + } + } + + if (C_alarmType == "3") { + C_sonos.playTrack(state.selectedSongC) + } + } +} + +def alarm_D() { + if ((!D_mode || D_mode.contains(location.mode)) && getDayOk(D_day)) { + if (D_switches || D_dimmers || D_thermostats) { + def dimLevel = D_level as Integer + D_switches?.on() + D_dimmers?.setLevel(dimLevel) + if (D_thermostats) { + def thermostatState = D_thermostats.currentThermostatMode + if (thermostatState == "auto") { + D_thermostats.setHeatingSetpoint(D_temperatureH) + D_thermostats.setCoolingSetpoint(D_temperatureC) + } + else if (thermostatState == "heat") { + D_thermostats.setHeatingSetpoint(D_temperatureH) + log.info "Set $D_thermostats Heat $D_temperatureH°" + } + else { + D_thermostats.setCoolingSetpoint(D_temperatureC) + log.info "Set $D_thermostats Cool $D_temperatureC°" + } + } + } + if (D_phrase) { + location.helloHome.execute(D_phrase) + } + + if (D_triggerMode && location.mode != D_triggerMode) { + if (location.modes?.find{it.name == D_triggerMode}) { + setLocationMode(D_triggerMode) + } + else { + log.debug "Unable to change to undefined mode '${D_triggerMode}'" + } + } + + if (D_volume) { + D_sonos.setLevel(D_volume) + } + + if (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1")) { + state.fullMsgD = "" + if (D_wakeMsg) { + getGreeting(D_wakeMsg, 4) + } + + if (D_weatherReport || D_humidity || D_includeTemp || D_localTemp) { + getWeatherReport(4, D_weatherReport, D_humidity, D_includeTemp, D_localTemp) + } + + if (D_includeSunrise || D_includeSunset) { + getSunriseSunset(4, D_includeSunrise, D_includeSunset) + } + + if ((D_switches || D_dimmers || D_thermostats) && D_confirmSwitches) { + getOnConfimation(D_switches, D_dimmers, D_thermostats, 4) + } + + if (D_phrase && D_confirmPhrase) { + getPhraseConfirmation(4, D_phrase) + } + + if (D_triggerMode && D_confirmMode){ + getModeConfirmation(D_triggerMode, 4) + } + + state.soundD = textToSpeech(state.fullMsgD, true) + } + + if (D_alarmType == "1"){ + if (D_secondAlarm == "1" && state.soundAlarmD){ + D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.soundD.uri) + } + if (D_secondAlarm == "2" && state.selectedSongD && state.soundAlarmD){ + D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.selectedSongD) + } + if (!D_secondAlarm){ + D_sonos.playTrack(state.soundAlarmD.uri) + } + } + + if (D_alarmType == "2") { + if (D_secondAlarmMusic && state.selectedSongD){ + D_sonos.playSoundAndTrack (state.soundD.uri, state.soundD.duration, state.selectedSongD) + } + else { + D_sonos.playTrack(state.soundD.uri) + } + } + + if (D_alarmType == "3") { + D_sonos.playTrack(state.selectedSongD) + } + } +} + +def appTouchHandler(evt){ + if (!summaryMode || summaryMode.contains(location.mode)) { + state.summaryMsg = "The following is a summary of the alarm settings. " + getSummary (A_alarmOn, ScenarioNameA, A_timeStart, 1) + getSummary (B_alarmOn, ScenarioNameB, B_timeStart, 2) + getSummary (C_alarmOn, ScenarioNameC, C_timeStart, 3) + getSummary (D_alarmOn, ScenarioNameD, D_timeStart, 4) + + log.debug "Summary message = ${state.summaryMsg}" + def summarySound = textToSpeech(state.summaryMsg, true) + if (summaryVolume) { + summarySonos.setLevel(summaryVolume) + } + summarySonos.playTrack(summarySound.uri) + } +} + +def getSummary (alarmOn, scenarioName, timeStart, num){ + if (alarmOn && scenarioName) { + state.summaryMsg = "${state.summaryMsg} Alarm ${num}, ${scenarioName}, set for ${parseDate(timeStart,"", "h:mm a" )}, is enabled. " + } + else if (summaryDisabled && !alarmOn && scenarioName) { + state.summaryMsg = "${state.summaryMsg} Alarm ${num}, ${scenarioName}, set for ${parseDate(timeStart,"", "h:mm a")}, is disabled. " + } + else if (summaryDisabled && !scenarioName) { + state.summaryMsg = "${state.summaryMsg} Alarm ${num} is not configured. " + } +} + +//-------------------------------------- + +def getDesc(timeStart, sonos, day, mode) { + def desc = "Tap to set alarm" + if (timeStart) { + desc = "Alarm set to " + parseDate(timeStart,"", "h:mm a") +" on ${sonos}" + + def dayListSize = day ? day.size() : 7 + + if (day && dayListSize < 7) { + desc = desc + " on" + for (dayName in day) { + desc = desc + " ${dayName}" + dayListSize = dayListSize -1 + if (dayListSize) { + desc = "${desc}, " + } + } + } + else { + desc = desc + " every day" + } + + if (mode) { + def modeListSize = mode.size() + def modePrefix =" in the following modes: " + if (modeListSize == 1) { + modePrefix = " in the following mode: " + } + desc = desc + "${modePrefix}" + for (modeName in mode) { + desc = desc + "'${modeName}'" + modeListSize = modeListSize -1 + if (modeListSize) { + desc = "${desc}, " + } + else { + desc = "${desc}" + } + } + } + else { + desc = desc + " in all modes" + } + } + desc +} +def greyOut(scenario, sonos, alarmTime, alarmOn, alarmType){ + def result = scenario && sonos && alarmTime && alarmOn && alarmType ? "complete" : "" +} + +def greyOut1(param1, param2, param3, param4, param5, param6){ + def result = param1 || param2 || param3 || param4 || param5 || param6 ? "complete" : "" +} + +def getWeatherDesc(param1, param2, param3, param4, param5, param6) { + def title = param1 || param2 || param3 || param4 || param5 || param6 ? "Tap to edit weather reporting options" : "Tap to setup weather reporting options" +} + +def greyOutOption(param){ + def result = param ? "complete" : "" +} + +def getTitle(scenario, num) { + def title = scenario ? scenario : "Alarm ${num} not configured" +} + +def dimmerDesc(dimmer){ + def desc = dimmer ? "Tap to edit dimmer settings" : "Tap to set dimmer setting" +} + +def thermostatDesc(thermostat, heating, cooling){ + def tempText + if (heating || cooling){ + if (heating){ + tempText = "${heating} heat" + } + if (cooling){ + tempText = "${cooling} cool" + } + if (heating && cooling) { + tempText ="${heating} heat / ${cooling} cool" + } + } + else { + tempText="Tap to edit thermostat settings" + } + + def desc = thermostat ? "${tempText}" : "Tap to set thermostat settings" + return desc +} + +private getDayOk(dayList) { + def result = true + if (dayList) { + result = dayList.contains(getDay()) + } + result +} + +private getDay(){ + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) +} + +private parseDate(date, epoch, type){ + def parseDate = "" + if (epoch){ + long longDate = Long.valueOf(epoch).longValue() + parseDate = new Date(longDate).format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone) + } + else { + parseDate = date + } + new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", parseDate).format("${type}", timeZone(parseDate)) +} + +private getSunriseSunset(scenario, includeSunrise, includeSunset){ + if (location.timeZone || zipCode) { + def todayDate = new Date() + def s = getSunriseAndSunset(zipcode: zipCode, date: todayDate) + def riseTime = parseDate("", s.sunrise.time, "h:mm a") + def setTime = parseDate ("", s.sunset.time, "h:mm a") + def msg = "" + def currTime = now() + def verb1 = currTime >= s.sunrise.time ? "rose" : "will rise" + def verb2 = currTime >= s.sunset.time ? "set" : "will set" + + if (includeSunrise && includeSunset) { + msg = "The sun ${verb1} this morning at ${riseTime} and ${verb2} at ${setTime}. " + } + else if (includeSunrise && !includeSunset) { + msg = "The sun ${verb1} this morning at ${riseTime}. " + } + else if (!includeSunrise && includeSunset) { + msg = "The sun ${verb2} tonight at ${setTime}. " + } + compileMsg(msg, scenario) + } + else { + msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information. " + compileMsg(msg, scenario) + } +} + +private getGreeting(msg, scenario) { + def day = getDay() + def time = parseDate("", now(), "h:mm a") + def month = parseDate("", now(), "MMMM") + def year = parseDate("", now(), "yyyy") + def dayNum = parseDate("", now(), "dd") + msg = msg.replace('%day%', day) + msg = msg.replace('%date%', "${month} ${dayNum}, ${year}") + msg = msg.replace('%time%', "${time}") + msg = "${msg} " + compileMsg(msg, scenario) +} + +private getWeatherReport(scenario, weatherReport, humidity, includeTemp, localTemp) { + if (location.timeZone || zipCode) { + def isMetric = location.temperatureScale == "C" + def sb = new StringBuilder() + + if (includeTemp){ + def current = getWeatherFeature("conditions", zipCode) + if (isMetric) { + sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees. " + } + else { + sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees. " + } + } + + if (localTemp){ + sb << "The local temperature is ${Math.round(localTemp.currentTemperature)} degrees. " + } + + if (humidity) { + sb << "The local relative humidity is ${humidity.currentValue("humidity")}%. " + } + + if (weatherReport) { + def weather = getWeatherFeature("forecast", zipCode) + + sb << "Today's forecast is " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[0].fcttext + } + } + + def msg = sb.toString() + msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') + msg = msg.replaceAll(/([0-9]+)F/,'$1 degrees') + compileMsg(msg, scenario) + } + else { + msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts." + compileMsg(msg, scenario) + } +} + +private getOnConfimation(switches, dimmers, thermostats, scenario) { + def msg = "" + if ((switches || dimmers) && !thermostats) { + msg = "All switches" + } + if (!switches && !dimmers && thermostats) { + msg = "All Thermostats" + } + if ((switches || dimmers) && thermostats) { + msg = "All switches and thermostats" + } + msg = "${msg} are now on and set. " + compileMsg(msg, scenario) +} + +private getPhraseConfirmation(scenario, phrase) { + def msg="The Smart Things Hello Home phrase, ${phrase}, has been activated. " + compileMsg(msg, scenario) +} + +private getModeConfirmation(mode, scenario) { + def msg="The Smart Things mode is now being set to, ${mode}. " + compileMsg(msg, scenario) +} + +private compileMsg(msg, scenario) { + log.debug "msg = ${msg}" + if (scenario == 1) {state.fullMsgA = state.fullMsgA + "${msg}"} + if (scenario == 2) {state.fullMsgB = state.fullMsgB + "${msg}"} + if (scenario == 3) {state.fullMsgC = state.fullMsgC + "${msg}"} + if (scenario == 4) {state.fullMsgD = state.fullMsgD + "${msg}"} +} + +private alarmSoundUri(selection, length, scenario){ + def soundUri = "" + def soundLength = "" + switch(selection) { + case "1": + soundLength = length >0 && length < 8 ? length : 8 + soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmAlien.mp3", duration: "${soundLength}"] + break + case "2": + soundLength = length >0 && length < 12 ? length : 12 + soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmBell.mp3", duration: "${soundLength}"] + break + case "3": + soundLength = length >0 && length < 20 ? length : 20 + soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmBuzzer.mp3", duration: "${soundLength}"] + break + case "4": + soundLength = length >0 && length < 20 ? length : 20 + soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmFire.mp3", duration: "${soundLength}"] + break + case "5": + soundLength = length >0 && length < 2 ? length : 2 + soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmRooster.mp3", duration: "${soundLength}"] + break + case "6": + soundLength = length >0 && length < 20 ? length : 20 + soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmSiren.mp3", duration: "${soundLength}"] + break + } + if (scenario == 1) {state.soundAlarmA = soundUri} + if (scenario == 2) {state.soundAlarmB = soundUri} + if (scenario == 3) {state.soundAlarmC = soundUri} + if (scenario == 4) {state.soundAlarmD = soundUri} +} + +//Sonos Aquire Track from SmartThings code +private songOptions(sonos, scenario) { + if (sonos){ + // Make sure current selection is in the set + def options = new LinkedHashSet() + if (scenario == 1){ + if (state.selectedSongA?.station) { + options << state.selectedSongA.station + } + else if (state.selectedSongA?.description) { + options << state.selectedSongA.description + } + } + if (scenario == 2){ + if (state.selectedSongB?.station) { + options << state.selectedSongB.station + } + else if (state.selectedSongB?.description) { + options << state.selectedSongB.description + } + } + if (scenario == 3){ + if (state.selectedSongC?.station) { + options << state.selectedSongC.station + } + else if (state.selectedSongC?.description) { + options << state.selectedSongC.description + } + } + if (scenario == 4){ + if (state.selectedSongD?.station) { + options << state.selectedSongD.station + } + else if (state.selectedSongD?.description) { + options << state.selectedSongD.description + } + } + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List + } +} + +private saveSelectedSong(sonos, song, scenario) { + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + if (scenario == 1) {state.selectedSongA = data} + if (scenario == 2) {state.selectedSongB = data} + if (scenario == 3) {state.selectedSongC = data} + if (scenario == 4) {state.selectedSongD = data} + log.debug "Selected song for Scenario ${scenario} = ${data}" + } + else if (song == state.selectedSongA?.station || song == state.selectedSongB?.station || song == state.selectedSongC?.station || song == state.selectedSongD?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } +} + +//Version/Copyright/Information/Help + +private def textAppName() { + def text = "Talking Alarm Clock" +} + +private def textVersion() { + def text = "Version 1.4.5 (06/17/2015)" +} + +private def textCopyright() { + def text = "Copyright © 2015 Michael Struck" +} + +private def textLicense() { + def text = + "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"+ + "\n\n"+ + " http://www.apache.org/licenses/LICENSE-2.0"+ + "\n\n"+ + "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." +} + +private def textHelp() { + def text = + "Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with " + + "switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time. "+ + "You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report. " + + "Variables that can be used in the voice greeting include %day%, %time% and %date%.\n\n"+ + "From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will "+ + "speak a summary of the alarms enabled or disabled without having to go into the application itself. This " + + "functionality is optional and can be configured from the main setup page." +} + diff --git a/smartapps/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy b/smartapps/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy new file mode 100644 index 00000000000..bf739f154c1 --- /dev/null +++ b/smartapps/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy @@ -0,0 +1,80 @@ +/** + * Lights Off with No Motion and Presence + * + * Author: Bruce Adelsman + */ + +definition( + name: "Lights Off with No Motion and Presence", + namespace: "naissan", + author: "Bruce Adelsman", + description: "Turn lights off when no motion and presence is detected for a set period of time.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet@2x.png" +) + +preferences { + section("Light switches to turn off") { + input "switches", "capability.switch", title: "Choose light switches", multiple: true + } + section("Turn off when there is no motion and presence") { + input "motionSensor", "capability.motionSensor", title: "Choose motion sensor" + input "presenceSensors", "capability.presenceSensor", title: "Choose presence sensors", multiple: true + } + section("Delay before turning off") { + input "delayMins", "number", title: "Minutes of inactivity?" + } +} + +def installed() { + subscribe(motionSensor, "motion", motionHandler) + subscribe(presenceSensors, "presence", presenceHandler) +} + +def updated() { + unsubscribe() + subscribe(motionSensor, "motion", motionHandler) + subscribe(presenceSensors, "presence", presenceHandler) +} + +def motionHandler(evt) { + log.debug "handler $evt.name: $evt.value" + if (evt.value == "inactive") { + runIn(delayMins * 60, scheduleCheck, [overwrite: false]) + } +} + +def presenceHandler(evt) { + log.debug "handler $evt.name: $evt.value" + if (evt.value == "not present") { + runIn(delayMins * 60, scheduleCheck, [overwrite: false]) + } +} + +def isActivePresence() { + // check all the presence sensors, make sure none are present + def noPresence = presenceSensors.find{it.currentPresence == "present"} == null + !noPresence +} + +def scheduleCheck() { + log.debug "scheduled check" + def motionState = motionSensor.currentState("motion") + if (motionState.value == "inactive") { + def elapsed = now() - motionState.rawDateCreated.time + def threshold = 1000 * 60 * delayMins - 1000 + if (elapsed >= threshold) { + if (!isActivePresence()) { + log.debug "Motion has stayed inactive since last check ($elapsed ms) and no presence: turning lights off" + switches.off() + } else { + log.debug "Presence is active: do nothing" + } + } else { + log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): do nothing" + } + } else { + log.debug "Motion is active: do nothing" + } +} \ No newline at end of file diff --git a/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy b/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy new file mode 100644 index 00000000000..1af9a2d4285 --- /dev/null +++ b/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy @@ -0,0 +1,148 @@ +/** + * Smart Timer + * Loosely based on "Light Follows Me" + * + * This prevent them from turning off when the timer expires, if they were already turned on + * + * If the switch is already on, if won't be affected by the timer (Must be turned of manually) + * If the switch is toggled while in timeout-mode, it will remain on and ignore the timer (Must be turned of manually) + * + * The timeout perid begins when the contact is closed, or motion stops, so leaving a door open won't start the timer until it's closed. + * + * Author: andersheie@gmail.com + * Date: 2014-08-31 + */ + +definition( + name: "Smart Light Timer, X minutes unless already on", + namespace: "Pope", + author: "listpope@cox.net", + description: "Turns on a switch for X minutes, then turns it off. Unless, the switch is already on, in which case it stays on. If the switch is toggled while the timer is running, the timer is canceled.", + category: "Convenience", + iconUrl: "http://upload.wikimedia.org/wikipedia/commons/6/6a/Light_bulb_icon_tips.svg", + iconX2Url: "http://upload.wikimedia.org/wikipedia/commons/6/6a/Light_bulb_icon_tips.svg") + +preferences { + section("Turn on when there's movement..."){ + input "motions", "capability.motionSensor", multiple: true, title: "Select motion detectors", required: false + } + section("Or, turn on when one of these contacts opened"){ + input "contacts", "capability.contactSensor", multiple: true, title: "Select Contacts", required: false + } + section("And off after no more triggers after..."){ + input "minutes1", "number", title: "Minutes?", defaultValue: "5" + } + section("Turn on/off light(s)..."){ + input "switches", "capability.switch", multiple: true, title: "Select Lights" + } +} + + +def installed() +{ + subscribe(switches, "switch", switchChange) + subscribe(motions, "motion", motionHandler) + subscribe(contacts, "contact", contactHandler) + schedule("0 * * * * ?", "scheduleCheck") + state.myState = "ready" +} + + +def updated() +{ + unsubscribe() + subscribe(motions, "motion", motionHandler) + subscribe(switches, "switch", switchChange) + subscribe(contacts, "contact", contactHandler) + + state.myState = "ready" + log.debug "state: " + state.myState +} + +def switchChange(evt) { + log.debug "SwitchChange: $evt.name: $evt.value" + + if(evt.value == "on") { + // Slight change of Race condition between motion or contact turning the switch on, + // versus user turning the switch on. Since we can't pass event parameters :-(, we rely + // on the state and hope the best. + if(state.myState == "activating") { + // OK, probably an event from Activating something, and not the switch itself. Go to Active mode. + state.myState = "active" + } else if(state.myState != "active") { + state.myState = "already on" + } + } else { + // If active and switch is turned of manually, then stop the schedule and go to ready state + if(state.myState == "active" || state.myState == "activating") { + unschedule() + } + state.myState = "ready" + } + log.debug "state: " + state.myState +} + +def contactHandler(evt) { + log.debug "contactHandler: $evt.name: $evt.value" + + if (evt.value == "open") { + if(state.myState == "ready") { + log.debug "Turning on lights by contact opening" + switches.on() + state.inactiveAt = null + state.myState = "activating" + } + } else if (evt.value == "closed") { + if (!state.inactiveAt && state.myState == "active" || state.myState == "activating") { + // When contact closes, we reset the timer if not already set + setActiveAndSchedule() + } + } + log.debug "state: " + state.myState +} + +def motionHandler(evt) { + log.debug "motionHandler: $evt.name: $evt.value" + + if (evt.value == "active") { + if(state.myState == "ready" || state.myState == "active" || state.myState == "activating" ) { + log.debug "turning on lights" + switches.on() + state.inactiveAt = null + state.myState = "activating" + } + } else if (evt.value == "inactive") { + if (!state.inactiveAt && state.myState == "active" || state.myState == "activating") { + // When Motion ends, we reset the timer if not already set + setActiveAndSchedule() + } + } + log.debug "state: " + state.myState +} + +def setActiveAndSchedule() { + unschedule() + state.myState = "active" + state.inactiveAt = now() + schedule("0 * * * * ?", "scheduleCheck") +} + +def scheduleCheck() { + log.debug "schedule check, ts = ${state.inactiveAt}" + if(state.myState != "already on") { + if(state.inactiveAt != null) { + def elapsed = now() - state.inactiveAt + log.debug "${elapsed / 1000} sec since motion stopped" + def threshold = 1000 * 60 * minutes1 + if (elapsed >= threshold) { + if (state.myState == "active") { + log.debug "turning off lights" + switches.off() + } + state.inactiveAt = null + state.myState = "ready" + } + } + } + log.debug "state: " + state.myState +} diff --git a/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy b/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy new file mode 100644 index 00000000000..d1f3c1febcf --- /dev/null +++ b/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy @@ -0,0 +1,51 @@ +/** + * Monitor on Sense + * + * Copyright 2014 Rachel Steele + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Monitor on Sense", + namespace: "resteele", + author: "Rachel Steele", + description: "Turn on Monitor when vibration is sensed", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + oauth: [displayName: "Monitor on Vibrate", displayLink: ""]) + + +preferences { + section("When the keyboard is used...") { + input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?" + } +section("Turn on/off a light...") { + input "switch1", "capability.switch" + } +} + + +def installed() { + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def updated() { + unsubscribe() + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + + +def accelerationActiveHandler(evt) { + switch1.on() + } + + diff --git a/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy b/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy new file mode 100644 index 00000000000..c6b59053dd8 --- /dev/null +++ b/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy @@ -0,0 +1,208 @@ +/** + * HebcalModes + * + * Author: danielbarak@live.com + * Date: 2014-02-21 + */ + +// Automatically generated. Make future change here. +definition( + name: "Shabbat and Holiday Modes", + namespace: "ShabbatHolidayMode", + author: "danielbarak@live.com", + description: "Changes the mode at candle lighting and back after havdalah. Uses the HebCal.com API to look for days that are shabbat or chag and pull real time candle lighting and havdalah times to change modes automatically", + category: "My Apps", + iconUrl: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png", + iconX2Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png", + iconX3Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png") + +preferences { + + section("At Candlelighting Change Mode To:") + { + input "startMode", "mode", title: "Mode?" + } + section("At Havdalah Change Mode To:") + { + input "endMode", "mode", title: "Mode?" + } + section("Havdalah Offset (Usually 50 or 72)") { + input "havdalahOffset", "number", title: "Minutes After Sundown", required:true + } + section("Your ZipCode") { + input "zipcode", "number", title: "ZipCode", required:true + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "phone", "phone", title: "Send a Text Message?", required: false + } + /**/ +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + poll(); + schedule("0 0 8 1/1 * ? *", poll) +} + +//Check hebcal for today's candle lighting or havdalah +def poll() +{ + + unschedule("endChag") + unschedule("setChag") + Hebcal_WebRequest() + +}//END def poll() + + + +/********************************************** +// HEBCAL FUNCTIONS +-----------------------------------------------*/ + +//This function is the web request and response parse +def Hebcal_WebRequest(){ + +def today = new Date().format("yyyy-MM-dd") +//def today = "2014-11-14" +def zip = settings.zip as String +def locale = getWeatherFeature("geolookup", zip) +def timezone = TimeZone.getTimeZone(locale.location.tz_long) +def hebcal_date +def hebcal_category +def hebcal_title +def candlelighting +def candlelightingLocalTime +def havdalah +def havdalahLocalTime +def pushMessage +def testmessage +def urlRequest = "http://www.hebcal.com/hebcal/?v=1&cfg=json&nh=off&nx=off&year=now&month=now&mf=off&c=on&zip=${zipcode}&m=${havdalahOffset}&s=off&D=off&d=off&o=off&ss=off" +log.trace "${urlRequest}" + +def hebcal = { response -> + hebcal_date = response.data.items.date + hebcal_category = response.data.items.category + hebcal_title = response.data.items.title + + for (int i = 0; i < hebcal_date.size; i++) + { + if(hebcal_date[i].split("T")[0]==today) + { + if(hebcal_category[i]=="candles") + { + candlelightingLocalTime = HebCal_GetTime12(hebcal_title[i]) + pushMessage = "Candle Lighting is at ${candlelightingLocalTime}" + candlelightingLocalTime = HebCal_GetTime24(hebcal_date[i]) + candlelighting = timeToday(candlelightingLocalTime, timezone) + + sendMessage(pushMessage) + schedule(candlelighting, setChag) + log.debug pushMessage + }//END if(hebcal_category=="candles") + + else if(hebcal_category[i]=="havdalah") + { + havdalahLocalTime = HebCal_GetTime12(hebcal_title[i]) + pushMessage = "Havdalah is at ${havdalahLocalTime}" + havdalahLocalTime = HebCal_GetTime24(hebcal_date[i]) + havdalah = timeToday(havdalahLocalTime, timezone) + testmessage = "Scheduling for ${havdalah}" + schedule(havdalah, endChag) + log.debug pushMessage + log.debug testmessage + }//END if(hebcal_category=="havdalah"){ + }//END if(hebcal_date[i].split("T")[0]==today) + + }//END for (int i = 0; i < hebcal_date.size; i++) + }//END def hebcal = { response -> +httpGet(urlRequest, hebcal); +}//END def queryHebcal() + + +//This function gets candle lighting time +def HebCal_GetTime12(hebcal_title){ +def returnTime = hebcal_title.split(":")[1] + ":" + hebcal_title.split(":")[2] + " " +return returnTime +}//END def HebCal_GetTime12() + +//This function gets candle lighting time +def HebCal_GetTime24(hebcal_date){ +def returnTime = hebcal_date.split("T")[1] +returnTime = returnTime.split("-")[0] +return returnTime +}//END def HebCal_GetTime12() + +/*----------------------------------------------- + END OF HEBCAL FUNCTIONS +-----------------------------------------------*/ +def setChag() +{ + + if (location.mode != startMode) + { + if (location.modes?.find{it.name == startMode}) + { + setLocationMode(startMode) + //sendMessage("Changed the mode to '${startMode}'") + def dayofweek = new Date().format("EEE") + if(dayofweek=='Fri'){ + sendMessage("Shabbat Shalom!") + } + else{ + sendMessage("Chag Sameach!") + } + + }//END if (location.modes?.find{it.name == startMode}) + else + { + sendMessage("Tried to change to undefined mode '${startMode}'") + }//END else + }//END if (location.mode != newMode) + + unschedule("setChag") +}//END def setChag() + + +def endChag() +{ + + if (location.mode != endMode) + { + if (location.modes?.find{it.name == endMode}) + { + setLocationMode(endMode) + sendMessage("Changed the mode to '${endMode}'") + }//END if (location.modes?.find{it.name == endMode}) + else + { + sendMessage("Tried to change to undefined mode '${endMode}'") + }//END else + }//END if (location.mode != endMode) + + //sendMessage("Shavuah Tov!") + unschedule("endChag") +}//END def setChag() + +def sendMessage(msg){ +if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + //sendPush( msg ) + } + + if ( phone ) { + log.debug( "sending text message" ) + sendSms( phone, msg ) + } +}//END def sendMessage(msg) \ No newline at end of file diff --git a/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy b/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy new file mode 100644 index 00000000000..c489a220759 --- /dev/null +++ b/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy @@ -0,0 +1,128 @@ +/** + * Smart Humidifier + * + * Copyright 2014 Sheikh Dawood + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Smart Humidifier", + namespace: "Sheikhsphere", + author: "Sheikh Dawood", + description: "Turn on/off humidifier based on relative humidity from a sensor.", + category: "Convenience", + iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn", + iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn?displaySize=2x" +) + + +preferences { + section("Monitor the humidity of:") { + input "humiditySensor1", "capability.relativeHumidityMeasurement" + } + section("When the humidity rises above:") { + input "humidityHigh", "number", title: "Percentage ?" + } + section("When the humidity drops below:") { + input "humidityLow", "number", title: "Percentage ?" + } + section("Control Humidifier:") { + input "switch1", "capability.switch" + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "phone1", "phone", title: "Send a Text Message?", required: false + } +} + +def installed() { + subscribe(humiditySensor1, "humidity", humidityHandler) +} + +def updated() { + unsubscribe() + subscribe(humiditySensor1, "humidity", humidityHandler) +} + +def humidityHandler(evt) { + log.trace "humidity: $evt.value" + log.trace "set high point: $humidityHigh" + log.trace "set low point: $humidityLow" + + def currentHumidity = Double.parseDouble(evt.value.replace("%", "")) + def humidityHigh1 = humidityHigh + def humidityLow1 = humidityLow + def mySwitch = settings.switch1 + + if (currentHumidity >= humidityHigh1) { + log.debug "Checking how long the humidity sensor has been reporting >= $humidityHigh1" + + // Don't send a continuous stream of text messages + def deltaMinutes = 10 + def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong()) + def recentEvents = humiditySensor1.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes" + def alreadySentSms1 = recentEvents.count { Double.parseDouble(it.value.replace("%", "")) >= humidityHigh1 } > 1 + + if (alreadySentSms1) { + log.debug "Notification already sent within the last $deltaMinutes minutes" + + } else { + if (state.lastStatus != "off") { + log.debug "Humidity Rose Above $humidityHigh1: sending SMS to $phone1 and deactivating $mySwitch" + send("${humiditySensor1.label} sensed high humidity level of ${evt.value}, turning off ${switch1.label}") + switch1?.off() + state.lastStatus = "off" + } + } + } + else if (currentHumidity <= humidityLow1) { + log.debug "Checking how long the humidity sensor has been reporting <= $humidityLow1" + + // Don't send a continuous stream of text messages + def deltaMinutes = 10 + def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong()) + def recentEvents = humiditySensor1.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes" + def alreadySentSms2 = recentEvents.count { Double.parseDouble(it.value.replace("%", "")) <= humidityLow1 } > 1 + + if (alreadySentSms2) { + log.debug "Notification already sent within the last $deltaMinutes minutes" + + } else { + if (state.lastStatus != "on") { + log.debug "Humidity Dropped Below $humidityLow1: sending SMS to $phone1 and activating $mySwitch" + send("${humiditySensor1.label} sensed low humidity level of ${evt.value}, turning on ${switch1.label}") + switch1?.on() + state.lastStatus = "on" + } + } + } + else { + //log.debug "Humidity remained in threshold: sending SMS to $phone1 and activating $mySwitch" + //send("${humiditySensor1.label} sensed humidity level of ${evt.value} is within threshold, keeping on ${switch1.label}") + //switch1?.on() + } +} + +private send(msg) { + if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + sendPush( msg ) + } + + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + + log.debug msg +} \ No newline at end of file diff --git a/smartapps/sidjohn1/smart-turn-it-on.src/smart-turn-it-on.groovy b/smartapps/sidjohn1/smart-turn-it-on.src/smart-turn-it-on.groovy new file mode 100644 index 00000000000..8e31d2aa215 --- /dev/null +++ b/smartapps/sidjohn1/smart-turn-it-on.src/smart-turn-it-on.groovy @@ -0,0 +1,73 @@ +/** + * Smart turn it on + * + * Author: sidjohn1@gmail.com + * Date: 2013-10-21 + */ + +// Automatically generated. Make future change here. +definition( + name: "Smart turn it on", + namespace: "sidjohn1", + author: "sidjohn1@gmail.com", + description: "Turns on selected device(s) at a set time on selected days of the week only if a selected person is present and turns off selected device(s) after a set time.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("Turn on which device?"){ + input "switchOne", "capability.switch",title:"Select Light", required: true, multiple: true + } + section("For Whom?") { + input "presenceOne", "capability.presenceSensor", title: "Select Person", required: true, multiple: true + } + section("On which Days?") { + input "dayOne", "enum", title:"Select Days", required: true, multiple:true, metadata: [values: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']] + } + section("At what time?") { + input name: "timeOne", title: "Select Time", type: "time", required: true + } + section("For how long?") { + input name: "timeTwo", title: "Number of minutes", type: "number", required: true + } +} + +def installed() { + if (timeOne) + { + log.debug "scheduling 'Smart turn it on' to run at $timeOne" + schedule(timeOne, "turnOn") + } +} + +def updated() { + unsubscribe() + unschedule() + if (timeOne) + { + log.debug "scheduling 'Smart turn it on' to run at $timeOne" + schedule(timeOne, "turnOn") + } +} + +def turnOn(){ +log.debug "Start" + def dayCheck = dayOne.contains(new Date().format("EEE")) + def dayTwo = new Date().format("EEE"); + if(dayCheck){ + def presenceTwo = presenceOne.latestValue("presence").contains("present") + if (presenceTwo) { + switchOne.on() + def delay = timeTwo * 60 + runIn(delay, "turnOff") + } + } +} + + + +def turnOff() { + switchOne.off() +} \ No newline at end of file diff --git a/smartapps/skp19/door-lock-code-distress-message.src/door-lock-code-distress-message.groovy b/smartapps/skp19/door-lock-code-distress-message.src/door-lock-code-distress-message.groovy new file mode 100644 index 00000000000..35d459bdec2 --- /dev/null +++ b/smartapps/skp19/door-lock-code-distress-message.src/door-lock-code-distress-message.groovy @@ -0,0 +1,58 @@ +/** + * Door Lock Code Distress Message + * + * Copyright 2014 skp19 + * + */ +definition( + name: "Door Lock Code Distress Message", + namespace: "skp19", + author: "skp19", + description: "Sends a text to someone when a specific code is entered", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + +import groovy.json.JsonSlurper + +preferences { + section("Choose Locks") { + input "lock1", "capability.lock", multiple: true + } + section("Enter User Code Number (This is not the code used to unlock the door)") { + input "distressCode", "number", defaultValue: "0" + } + section("Distress Message Details") { + input "phone1", "phone", title: "Phone number to send message to" + input "distressMsg", "text", title: "Message to send" + } + section("User Code Discovery Mode (Enable and unlock the door using desired code. A message will be sent containing the user code used to unlock the door.)") { + input "discoveryMode", "bool", title: "Enable" + } +} + +def installed() { + subscribe(lock1, "lock", checkCode) +} + +def updated() { + unsubscribe() + subscribe(lock1, "lock", checkCode) +} + +def checkCode(evt) { + log.debug "$evt.value: $evt, $settings" + + if(evt.value == "unlocked" && evt.data) { + def lockData = new JsonSlurper().parseText(evt.data) + + if(discoveryMode) { + sendPush "Door unlocked with user code $lockData.usedCode" + } + + if(lockData.usedCode == distressCode && discoveryMode == false) { + log.info "Distress Message Sent" + sendSms(phone1, distressMsg) + } + } +} diff --git a/smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy b/smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy new file mode 100644 index 00000000000..7a0fbb587d8 --- /dev/null +++ b/smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy @@ -0,0 +1,156 @@ +/** + * Smart Lock / Unlock + * + * Copyright 2014 Arnaud + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Smart Lock / Unlock", + namespace: "", + author: "Arnaud", + description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + +preferences +{ + section("Select the door lock:") { + input "lock1", "capability.lock", required: true + } + section("Select the door contact sensor:") { + input "contact1", "capability.contactSensor", required: true + } + section("Automatically lock the door when closed...") { + input "minutesLater", "number", title: "Delay (in minutes):", required: true + } + section("Automatically unlock the door when open...") { + input "secondsLater", "number", title: "Delay (in seconds):", required: true + } + section( "Push notification?" ) { + input "sendPushMessage", "enum", title: "Send push notification?", metadata:[values:["Yes", "No"]], required: false + } + section( "Text message?" ) { + input "sendText", "enum", title: "Send text message notification?", metadata:[values:["Yes", "No"]], required: false + input "phoneNumber", "phone", title: "Enter phone number:", required: false + } +} + +def installed() +{ + initialize() +} + +def updated() +{ + unsubscribe() + unschedule() + initialize() +} + +def initialize() +{ + log.debug "Settings: ${settings}" + subscribe(lock1, "lock", doorHandler, [filterEvents: false]) + subscribe(lock1, "unlock", doorHandler, [filterEvents: false]) + subscribe(contact1, "contact.open", doorHandler) + subscribe(contact1, "contact.closed", doorHandler) +} + +def lockDoor() +{ + if (lock1.latestValue("lock") == "unlocked") + { + log.debug "Locking $lock1..." + lock1.lock() + log.debug ("Sending Push Notification...") + if (sendPushMessage != "No") sendPush("$lock1 locked after $contact1 was closed for $minutesLater minute(s)!") + log.debug("Sending text message...") + if ((sendText == "Yes") && (phoneNumber != "0")) sendSms(phoneNumber, "$lock1 locked after $contact1 was closed for $minutesLater minute(s)!") + } + else if (lock1.latestValue("lock") == "locked") + { + log.debug "$lock1 was already locked..." + } +} + +def unlockDoor() +{ + if (lock1.latestValue("lock") == "locked") + { + log.debug "Unlocking $lock1..." + lock1.unlock() + log.debug ("Sending Push Notification...") + if (sendPushMessage != "No") sendPush("$lock1 unlocked after $contact1 was open for $secondsLater seconds(s)!") + log.debug("Sending text message...") + if ((sendText == "Yes") && (phoneNumber != "0")) sendSms(phoneNumber, "$lock1 unlocked after $contact1 was open for $secondsLater seconds(s)!") + } + else if (lock1.latestValue("lock") == "unlocked") + { + log.debug "$lock1 was already unlocked..." + } +} + +def doorHandler(evt) +{ + if ((contact1.latestValue("contact") == "open") && (evt.value == "locked")) + { + def delay = secondsLater + runIn (delay, unlockDoor) + } + else if ((contact1.latestValue("contact") == "open") && (evt.value == "unlocked")) + { + unschedule (unlockDoor) + } + else if ((contact1.latestValue("contact") == "closed") && (evt.value == "locked")) + { + unschedule (lockDoor) + } + else if ((contact1.latestValue("contact") == "closed") && (evt.value == "unlocked")) + { + log.debug "Unlocking $lock1..." + lock1.unlock() + def delay = (minutesLater * 60) + runIn (delay, lockDoor) + } + else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) + { + unschedule (lockDoor) + log.debug "Unlocking $lock1..." + lock1.unlock() + } + else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) + { + log.debug "Unlocking $lock1..." + lock1.unlock() + def delay = (minutesLater * 60) + runIn (delay, lockDoor) + } + else if ((lock1.latestValue("lock") == "locked") && (evt.value == "open")) + { + unschedule (lockDoor) + log.debug "Unlocking $lock1..." + lock1.unlock() + } + else if ((lock1.latestValue("lock") == "locked") && (evt.value == "closed")) + { + unschedule (lockDoor) + log.debug "Unlocking $lock1..." + lock1.unlock() + } + else + { + log.debug "Problem with $lock1, the lock might be jammed!" + unschedule (lockDoor) + unschedule (unlockDoor) + } +} \ No newline at end of file diff --git a/smartapps/smartthings/beacon-control.src/beacon-control.groovy b/smartapps/smartthings/beacon-control.src/beacon-control.groovy new file mode 100644 index 00000000000..17fddad5246 --- /dev/null +++ b/smartapps/smartthings/beacon-control.src/beacon-control.groovy @@ -0,0 +1,314 @@ +/** + * Beacon Control + * + * Copyright 2014 Physical Graph Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Beacon Control", + category: "SmartThings Internal", + namespace: "smartthings", + author: "SmartThings", + description: "Execute a Hello, Home phrase, turn on or off some lights, and/or lock or unlock your door when you enter or leave a monitored region", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol@2x.png" +) + +preferences { + page(name: "mainPage") + + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage", install: true, uninstall: true) { + + section("Where do you want to watch?") { + input name: "beacons", type: "capability.beacon", title: "Select your beacon(s)", + multiple: true, required: true + } + + section("Who do you want to watch for?") { + input name: "phones", type: "device.mobilePresence", title: "Select your phone(s)", + multiple: true, required: true + } + + section("What do you want to do on arrival?") { + input name: "arrivalPhrase", type: "enum", title: "Execute a phrase", + options: listPhrases(), required: false + input "arrivalOnSwitches", "capability.switch", title: "Turn on some switches", + multiple: true, required: false + input "arrivalOffSwitches", "capability.switch", title: "Turn off some switches", + multiple: true, required: false + input "arrivalLocks", "capability.lock", title: "Unlock the door", + multiple: true, required: false + } + + section("What do you want to do on departure?") { + input name: "departPhrase", type: "enum", title: "Execute a phrase", + options: listPhrases(), required: false + input "departOnSwitches", "capability.switch", title: "Turn on some switches", + multiple: true, required: false + input "departOffSwitches", "capability.switch", title: "Turn off some switches", + multiple: true, required: false + input "departLocks", "capability.lock", title: "Lock the door", + multiple: true, required: false + } + + section("Do you want to be notified?") { + input "pushNotification", "bool", title: "Send a push notification" + input "phone", "phone", title: "Send a text message", description: "Tap to enter phone number", + required: false + } + + section { + label title: "Give your automation a name", description: "e.g. Goodnight Home, Wake Up" + } + + def timeLabel = timeIntervalLabel() + section(title: "More options", hidden: hideOptionsSection(), hideable: true) { + href "timeIntervalInput", title: "Only during a certain time", + description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + } +} + +// Lifecycle management +def installed() { + log.debug " Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug " Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe(beacons, "presence", beaconHandler) +} + +// Event handlers +def beaconHandler(evt) { + log.debug " beaconHandler: $evt" + + if (allOk) { + def data = new groovy.json.JsonSlurper().parseText(evt.data) + log.debug " data: $data - phones: " + phones*.deviceNetworkId + + def beaconName = getBeaconName(evt) + log.debug " beaconName: $beaconName" + + def phoneName = getPhoneName(data) + log.debug " phoneName: $phoneName" + if (phoneName != null) { + def action = data.presence == "1" ? "arrived" : "left" + def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName" + + if (action == "arrived") { + msg = arriveActions(msg) + } + else if (action == "left") { + msg = departActions(msg) + } + log.debug " msg: $msg" + + if (pushNotification || phone) { + def options = [ + method: (pushNotification && phone) ? "both" : (pushNotification ? "push" : "sms"), + phone: phone + ] + sendNotification(msg, options) + } + } + } +} + +// Helpers +private arriveActions(msg) { + if (arrivalPhrase || arrivalOnSwitches || arrivalOffSwitches || arrivalLocks) msg += ", so" + + if (arrivalPhrase) { + log.debug " executing: $arrivalPhrase" + executePhrase(arrivalPhrase) + msg += " ${prefix('executed')} $arrivalPhrase." + } + if (arrivalOnSwitches) { + log.debug " turning on: $arrivalOnSwitches" + arrivalOnSwitches.on() + msg += " ${prefix('turned')} ${list(arrivalOnSwitches)} on." + } + if (arrivalOffSwitches) { + log.debug " turning off: $arrivalOffSwitches" + arrivalOffSwitches.off() + msg += " ${prefix('turned')} ${list(arrivalOffSwitches)} off." + } + if (arrivalLocks) { + log.debug " unlocking: $arrivalLocks" + arrivalLocks.unlock() + msg += " ${prefix('unlocked')} ${list(arrivalLocks)}." + } + msg +} + +private departActions(msg) { + if (departPhrase || departOnSwitches || departOffSwitches || departLocks) msg += ", so" + + if (departPhrase) { + log.debug " executing: $departPhrase" + executePhrase(departPhrase) + msg += " ${prefix('executed')} $departPhrase." + } + if (departOnSwitches) { + log.debug " turning on: $departOnSwitches" + departOnSwitches.on() + msg += " ${prefix('turned')} ${list(departOnSwitches)} on." + } + if (departOffSwitches) { + log.debug " turning off: $departOffSwitches" + departOffSwitches.off() + msg += " ${prefix('turned')} ${list(departOffSwitches)} off." + } + if (departLocks) { + log.debug " unlocking: $departLocks" + departLocks.lock() + msg += " ${prefix('locked')} ${list(departLocks)}." + } + msg +} + +private prefix(word) { + def result + def index = settings.prefixIndex == null ? 0 : settings.prefixIndex + 1 + switch (index) { + case 0: + result = "I $word" + break + case 1: + result = "I also $word" + break + case 2: + result = "And I $word" + break + default: + result = "And $word" + break + } + + settings.prefixIndex = index + log.trace "prefix($word'): $result" + result +} + +private listPhrases() { + location.helloHome.getPhrases().label +} + +private executePhrase(phraseName) { + if (phraseName) { + location.helloHome.execute(phraseName) + log.debug " executed phrase: $phraseName" + } +} + +private getBeaconName(evt) { + def beaconName = beacons.find { b -> b.id == evt.deviceId } + return beaconName +} + +private getPhoneName(data) { + def phoneName = phones.find { phone -> + // Work around DNI bug in data + def pParts = phone.deviceNetworkId.split('\\|') + def dParts = data.dni.split('\\|') + pParts[0] == dParts[0] + } + return phoneName +} + +private hideOptionsSection() { + (starting || ending || days || modes) ? false : true +} + +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace " modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace " daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace " timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") { + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() { + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} + +private list(List names) { + switch (names.size()) { + case 0: + return null + case 1: + return names[0] + case 2: + return "${names[0]} and ${names[1]}" + default: + return "${names[0..-2].join(', ')}, and ${names[-1]}" + } +} diff --git a/smartapps/smartthings/big-turn-off.src/big-turn-off.groovy b/smartapps/smartthings/big-turn-off.src/big-turn-off.groovy new file mode 100644 index 00000000000..f5fae99b57e --- /dev/null +++ b/smartapps/smartthings/big-turn-off.src/big-turn-off.groovy @@ -0,0 +1,54 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Big Turn OFF + * + * Author: SmartThings + */ +definition( + name: "Big Turn OFF", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights off when the SmartApp is tapped or activated", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section("When I touch the app, turn off...") { + input "switches", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(location, changedLocationMode) + subscribe(app, appTouch) +} + +def updated() +{ + unsubscribe() + subscribe(location, changedLocationMode) + subscribe(app, appTouch) +} + +def changedLocationMode(evt) { + log.debug "changedLocationMode: $evt" + switches?.off() +} + +def appTouch(evt) { + log.debug "appTouch: $evt" + switches?.off() +} diff --git a/smartapps/smartthings/big-turn-on.src/big-turn-on.groovy b/smartapps/smartthings/big-turn-on.src/big-turn-on.groovy new file mode 100644 index 00000000000..f395ab3314c --- /dev/null +++ b/smartapps/smartthings/big-turn-on.src/big-turn-on.groovy @@ -0,0 +1,55 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Big Turn ON + * + * Author: SmartThings + */ + +definition( + name: "Big Turn ON", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights on when the SmartApp is tapped or activated.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section("When I touch the app, turn on...") { + input "switches", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(location, changedLocationMode) + subscribe(app, appTouch) +} + +def updated() +{ + unsubscribe() + subscribe(location, changedLocationMode) + subscribe(app, appTouch) +} + +def changedLocationMode(evt) { + log.debug "changedLocationMode: $evt" + switches?.on() +} + +def appTouch(evt) { + log.debug "appTouch: $evt" + switches?.on() +} diff --git a/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy b/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy new file mode 100644 index 00000000000..a843a71f12d --- /dev/null +++ b/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy @@ -0,0 +1,145 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Bon Voyage + * + * Author: SmartThings + * Date: 2013-03-07 + * + * Monitors a set of presence detectors and triggers a mode change when everyone has left. + */ + +definition( + name: "Bon Voyage", + namespace: "smartthings", + author: "SmartThings", + description: "Monitors a set of SmartSense Presence tags or smartphones and triggers a mode change when everyone has left. Used in conjunction with Big Turn Off or Make It So to turn off lights, appliances, adjust the thermostat, turn on security apps, and more.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png" +) + +preferences { + section("When all of these people leave home") { + input "people", "capability.presenceSensor", multiple: true + } + section("Change to this mode") { + input "newMode", "mode", title: "Mode?" + } + section("False alarm threshold (defaults to 10 min)") { + input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to", required: false) { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } + } + +} + +def installed() { + log.debug "Installed with settings: ${settings}" + log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + subscribe(people, "presence", presence) +} + +def updated() { + log.debug "Updated with settings: ${settings}" + log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + unsubscribe() + subscribe(people, "presence", presence) +} + +def presence(evt) +{ + log.debug "evt.name: $evt.value" + if (evt.value == "not present") { + if (location.mode != newMode) { + log.debug "checking if everyone is away" + if (everyoneIsAway()) { + log.debug "starting sequence" + runIn(findFalseAlarmThreshold() * 60, "takeAction", [overwrite: false]) + } + } + else { + log.debug "mode is the same, not evaluating" + } + } + else { + log.debug "present; doing nothing" + } +} + +def takeAction() +{ + if (everyoneIsAway()) { + def threshold = 1000 * 60 * findFalseAlarmThreshold() - 1000 + def awayLongEnough = people.findAll { person -> + def presenceState = person.currentState("presence") + if (!presenceState) { + // This device has yet to check in and has no presence state, treat it as not away long enough + return false + } + def elapsed = now() - presenceState.rawDateCreated.time + elapsed >= threshold + } + log.debug "Found ${awayLongEnough.size()} out of ${people.size()} person(s) who were away long enough" + if (awayLongEnough.size() == people.size()) { + // TODO -- uncomment when app label is available + def message = "SmartThings changed your mode to '${newMode}' because everyone left home" + log.info message + send(message) + setLocationMode(newMode) + } else { + log.debug "not everyone has been away long enough; doing nothing" + } + } else { + log.debug "not everyone is away; doing nothing" + } +} + +private everyoneIsAway() +{ + def result = true + for (person in people) { + if (person.currentPresence == "present") { + result = false + break + } + } + log.debug "everyoneIsAway: $result" + return result +} + +private send(msg) { + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phone) { + log.debug("sending text message") + sendSms(phone, msg) + } + } + log.debug msg +} + +private findFalseAlarmThreshold() { + (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold : 10 +} diff --git a/smartapps/smartthings/brighten-dark-places.src/brighten-dark-places.groovy b/smartapps/smartthings/brighten-dark-places.src/brighten-dark-places.groovy new file mode 100644 index 00000000000..c8ab1d0de1d --- /dev/null +++ b/smartapps/smartthings/brighten-dark-places.src/brighten-dark-places.groovy @@ -0,0 +1,57 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Brighten Dark Places + * + * Author: SmartThings + */ +definition( + name: "Brighten Dark Places", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights on when a open/close sensor opens and the space is dark.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance@2x.png" +) + +preferences { + section("When the door opens...") { + input "contact1", "capability.contactSensor", title: "Where?" + } + section("And it's dark...") { + input "luminance1", "capability.illuminanceMeasurement", title: "Where?" + } + section("Turn on a light...") { + input "switch1", "capability.switch" + } +} + +def installed() +{ + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + def lightSensorState = luminance1.currentIlluminance + log.debug "SENSOR = $lightSensorState" + if (lightSensorState != null && lightSensorState < 10) { + log.trace "light.on() ... [luminance: ${lightSensorState}]" + switch1.on() + } +} diff --git a/smartapps/smartthings/brighten-my-path.src/brighten-my-path.groovy b/smartapps/smartthings/brighten-my-path.src/brighten-my-path.groovy new file mode 100644 index 00000000000..8f3eac83e7c --- /dev/null +++ b/smartapps/smartthings/brighten-my-path.src/brighten-my-path.groovy @@ -0,0 +1,49 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Brighten My Path + * + * Author: SmartThings + */ +definition( + name: "Brighten My Path", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights on when motion is detected.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet@2x.png" +) + +preferences { + section("When there's movement...") { + input "motion1", "capability.motionSensor", title: "Where?", multiple: true + } + section("Turn on a light...") { + input "switch1", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(motion1, "motion.active", motionActiveHandler) +} + +def updated() +{ + unsubscribe() + subscribe(motion1, "motion.active", motionActiveHandler) +} + +def motionActiveHandler(evt) { + switch1.on() +} diff --git a/smartapps/smartthings/button-controller.src/button-controller.groovy b/smartapps/smartthings/button-controller.src/button-controller.groovy new file mode 100644 index 00000000000..1f99b08c45e --- /dev/null +++ b/smartapps/smartthings/button-controller.src/button-controller.groovy @@ -0,0 +1,319 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Button Controller + * + * Author: SmartThings + * Date: 2014-5-21 + */ +definition( + name: "Button Controller", + namespace: "smartthings", + author: "SmartThings", + description: "Control devices with buttons like the Aeon Labs Minimote", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png" +) + +preferences { + page(name: "selectButton") + page(name: "configureButton1") + page(name: "configureButton2") + page(name: "configureButton3") + page(name: "configureButton4") + + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def selectButton() { + dynamicPage(name: "selectButton", title: "First, select your button device", nextPage: "configureButton1", uninstall: configured()) { + section { + input "buttonDevice", "capability.button", title: "Button", multiple: false, required: true + } + + section(title: "More options", hidden: hideOptionsSection(), hideable: true) { + + def timeLabel = timeIntervalLabel() + + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null + + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + } +} + +def configureButton1() { + dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button", + nextPage: "configureButton2", uninstall: configured(), getButtonSections(1)) +} +def configureButton2() { + dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here", + nextPage: "configureButton3", uninstall: configured(), getButtonSections(2)) +} + +def configureButton3() { + dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here", + nextPage: "configureButton4", uninstall: configured(), getButtonSections(3)) +} +def configureButton4() { + dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here", + install: true, uninstall: true, getButtonSections(4)) +} + +def getButtonSections(buttonNumber) { + return { + section("Lights") { + input "lights_${buttonNumber}_pushed", "capability.switch", title: "Pushed", multiple: true, required: false + input "lights_${buttonNumber}_held", "capability.switch", title: "Held", multiple: true, required: false + } + section("Locks") { + input "locks_${buttonNumber}_pushed", "capability.lock", title: "Pushed", multiple: true, required: false + input "locks_${buttonNumber}_held", "capability.lock", title: "Held", multiple: true, required: false + } + section("Sonos") { + input "sonos_${buttonNumber}_pushed", "capability.musicPlayer", title: "Pushed", multiple: true, required: false + input "sonos_${buttonNumber}_held", "capability.musicPlayer", title: "Held", multiple: true, required: false + } + section("Modes") { + input "mode_${buttonNumber}_pushed", "mode", title: "Pushed", required: false + input "mode_${buttonNumber}_held", "mode", title: "Held", required: false + } + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + section("Hello Home Actions") { + log.trace phrases + input "phrase_${buttonNumber}_pushed", "enum", title: "Pushed", required: false, options: phrases + input "phrase_${buttonNumber}_held", "enum", title: "Held", required: false, options: phrases + } + } + section("Sirens") { + input "sirens_${buttonNumber}_pushed","capability.alarm" ,title: "Pushed", multiple: true, required: false + input "sirens_${buttonNumber}_held", "capability.alarm", title: "Held", multiple: true, required: false + } + + section("Custom Message") { + input "textMessage_${buttonNumber}", "text", title: "Message", required: false + } + + section("Push Notifications") { + input "notifications_${buttonNumber}_pushed","bool" ,title: "Pushed", required: false, defaultValue: false + input "notifications_${buttonNumber}_held", "bool", title: "Held", required: false, defaultValue: false + } + + section("Sms Notifications") { + input "phone_${buttonNumber}_pushed","phone" ,title: "Pushed", required: false + input "phone_${buttonNumber}_held", "phone", title: "Held", required: false + } + } +} + +def installed() { + initialize() +} + +def updated() { + unsubscribe() + initialize() +} + +def initialize() { + subscribe(buttonDevice, "button", buttonEvent) +} + +def configured() { + return buttonDevice || buttonConfigured(1) || buttonConfigured(2) || buttonConfigured(3) || buttonConfigured(4) +} + +def buttonConfigured(idx) { + return settings["lights_$idx_pushed"] || + settings["locks_$idx_pushed"] || + settings["sonos_$idx_pushed"] || + settings["mode_$idx_pushed"] || + settings["notifications_$idx_pushed"] || + settings["sirens_$idx_pushed"] || + settings["notifications_$idx_pushed"] || + settings["phone_$idx_pushed"] +} + +def buttonEvent(evt){ + if(allOk) { + def buttonNumber = evt.data // why doesn't jsonData work? always returning [:] + def value = evt.value + log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)" + log.debug "button: $buttonNumber, value: $value" + + def recentEvents = buttonDevice.eventsSince(new Date(now() - 3000)).findAll{it.value == evt.value && it.data == evt.data} + log.debug "Found ${recentEvents.size()?:0} events in past 3 seconds" + + if(recentEvents.size <= 1){ + switch(buttonNumber) { + case ~/.*1.*/: + executeHandlers(1, value) + break + case ~/.*2.*/: + executeHandlers(2, value) + break + case ~/.*3.*/: + executeHandlers(3, value) + break + case ~/.*4.*/: + executeHandlers(4, value) + break + } + } else { + log.debug "Found recent button press events for $buttonNumber with value $value" + } + } +} + +def executeHandlers(buttonNumber, value) { + log.debug "executeHandlers: $buttonNumber - $value" + + def lights = find('lights', buttonNumber, value) + if (lights != null) toggle(lights) + + def locks = find('locks', buttonNumber, value) + if (locks != null) toggle(locks) + + def sonos = find('sonos', buttonNumber, value) + if (sonos != null) toggle(sonos) + + def mode = find('mode', buttonNumber, value) + if (mode != null) changeMode(mode) + + def phrase = find('phrase', buttonNumber, value) + if (phrase != null) location.helloHome.execute(phrase) + + def textMessage = findMsg('textMessage', buttonNumber) + + def notifications = find('notifications', buttonNumber, value) + if (notifications?.toBoolean()) sendPush(textMessage ?: "Button $buttonNumber was pressed" ) + + def phone = find('phone', buttonNumber, value) + if (phone != null) sendSms(phone, textMessage ?:"Button $buttonNumber was pressed") + + def sirens = find('sirens', buttonNumber, value) + if (sirens != null) toggle(sirens) +} + +def find(type, buttonNumber, value) { + def preferenceName = type + "_" + buttonNumber + "_" + value + def pref = settings[preferenceName] + if(pref != null) { + log.debug "Found: $pref for $preferenceName" + } + + return pref +} + +def findMsg(type, buttonNumber) { + def preferenceName = type + "_" + buttonNumber + def pref = settings[preferenceName] + if(pref != null) { + log.debug "Found: $pref for $preferenceName" + } + + return pref +} + +def toggle(devices) { + log.debug "toggle: $devices = ${devices*.currentValue('switch')}" + + if (devices*.currentValue('switch').contains('on')) { + devices.off() + } + else if (devices*.currentValue('switch').contains('off')) { + devices.on() + } + else if (devices*.currentValue('lock').contains('locked')) { + devices.unlock() + } + else if (devices*.currentValue('alarm').contains('off')) { + devices.siren() + } + else { + devices.on() + } +} + +def changeMode(mode) { + log.debug "changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes" + + if (location.mode != mode && location.modes?.find { it.name == mode }) { + setLocationMode(mode) + } +} + +// execution filter methods +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private hideOptionsSection() { + (starting || ending || days || modes) ? false : true +} + +private timeIntervalLabel() { + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} diff --git a/smartapps/smartthings/camera-power-scheduler.src/camera-power-scheduler.groovy b/smartapps/smartthings/camera-power-scheduler.src/camera-power-scheduler.groovy new file mode 100644 index 00000000000..40fe5219902 --- /dev/null +++ b/smartapps/smartthings/camera-power-scheduler.src/camera-power-scheduler.groovy @@ -0,0 +1,88 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Schedule the Camera Power + * + * Author: danny@smartthings.com + * Date: 2013-10-07 + */ + +definition( + name: "Camera Power Scheduler", + namespace: "smartthings", + author: "SmartThings", + description: "Turn the power on and off at a specific time. ", + category: "Available Beta Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-schedule.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-schedule@2x.png" +) + +preferences { + section("Camera power..."){ + input "switch1", "capability.switch", multiple: true + } + section("Turn the Camera On at..."){ + input "startTime", "time", title: "Start Time", required:false + } + section("Turn the Camera Off at..."){ + input "endTime", "time", title: "End Time", required:false + } +} + +def installed() +{ + initialize() +} + +def updated() +{ + unschedule() + initialize() +} + +def initialize() { + /* + def tz = location.timeZone + + //if it's after the startTime but before the end time, turn it on + if(startTime && timeToday(startTime,tz).time > timeToday(now,tz).time){ + + if(endTime && timeToday(endTime,tz).time < timeToday(now,tz).time){ + switch1.on() + } + else{ + switch1.off() + } + } + else if(endTime && timeToday(endtime,tz).time > timeToday(now,tz).time) + { + switch1.off() + } + */ + + if(startTime) + runDaily(startTime, turnOnCamera) + if(endTime) + runDaily(endTime,turnOffCamera) +} + +def turnOnCamera() +{ + log.info "turned on camera" + switch1.on() +} + +def turnOffCamera() +{ + log.info "turned off camera" + switch1.off() +} diff --git a/smartapps/smartthings/cameras-on-when-im-away.src/cameras-on-when-im-away.groovy b/smartapps/smartthings/cameras-on-when-im-away.src/cameras-on-when-im-away.groovy new file mode 100644 index 00000000000..42051ba6d66 --- /dev/null +++ b/smartapps/smartthings/cameras-on-when-im-away.src/cameras-on-when-im-away.groovy @@ -0,0 +1,101 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Cameras On When I'm Away + * + * Author: danny@smartthings.com + * Date: 2013-10-07 + */ + +definition( + name: "Cameras On When I'm Away", + namespace: "smartthings", + author: "SmartThings", + description: "Turn cameras on when I'm away", + category: "Available Beta Apps", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-presence.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-presence@2x.png" +) + +preferences { + section("When all of these people are home...") { + input "people", "capability.presenceSensor", multiple: true + } + section("Turn off camera power..."){ + input "switches1", "capability.switch", multiple: true + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + log.debug "Current people = ${people.collect{it.label + ': ' + it.currentPresence}}" + subscribe(people, "presence", presence) +} + +def updated() { + log.debug "Updated with settings: ${settings}" + log.debug "Current people = ${people.collect{it.label + ': ' + it.currentPresence}}" + unsubscribe() + subscribe(people, "presence", presence) +} + +def presence(evt) +{ + log.debug "evt.name: $evt.value" + if (evt.value == "not present") { + + log.debug "checking if everyone is away" + if (everyoneIsAway()) { + log.debug "starting on Sequence" + + runIn(60*2, "turnOn") //two minute delay after everyone has left + } + } + else { + if (!everyoneIsAway()) { + turnOff() + } + } +} + +def turnOff() +{ + log.debug "canceling On requests" + unschedule("turnOn") + + log.info "turning off the camera" + switches1.off() +} + +def turnOn() +{ + + log.info "turned on the camera" + switches1.on() + + unschedule("turnOn") // Temporary work-around to scheduling bug +} + +private everyoneIsAway() +{ + def result = true + for (person in people) { + if (person.currentPresence == "present") { + result = false + break + } + } + log.debug "everyoneIsAway: $result" + return result +} + + diff --git a/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy b/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy new file mode 100644 index 00000000000..8edaad47556 --- /dev/null +++ b/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy @@ -0,0 +1,114 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * title: Carpool Notifier + * + * description: + * Do you carpool to work with your spouse? Do you pick your children up from school? Have they been waiting in doors for you? Let them know you've arrived with Carpool Notifier. + * + * This SmartApp is designed to send notifications to your carpooling buddies when you arrive to pick them up. What separates this SmartApp from other notification SmartApps is that it will only send a notification if your carpool buddy is not with you. + * + * category: Family + + * icon: https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png + * icon2X: https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt%402x.png + * + * Author: steve + * Date: 2013-11-19 + */ + +definition( + name: "Carpool Notifier", + namespace: "smartthings", + author: "SmartThings", + description: "This SmartApp is designed to send notifications to your carpooling buddies when you arrive to pick them up. What separates this SmartApp from other notification SmartApps is that it will only send a notification if your carpool buddy is not with you. If the person you are picking up is present, and has been for 5 minutes or more, they will get a notification when you become present.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt@2x.png" +) + +preferences { + section() { + input(name: "driver", type: "capability.presenceSensor", required: true, multiple: false, title: "When this person arrives", description: "Who's driving?") + input("recipients", "contact", title: "Notify", description: "Send notifications to") { + input(name: "phoneNumber", type: "phone", required: true, multiple: false, title: "Send a text to", description: "Phone number") + } + input(name: "message", type: "text", required: false, multiple: false, title: "With the message:", description: "Your ride is here!") + input(name: "rider", type: "capability.presenceSensor", required: true, multiple: false, title: "But only when this person is not with you", description: "Who are you picking up?") + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(driver, "presence.present", presence) +} + +def presence(evt) { + + if (evt.value == "present" && riderIsHome()) + { +// log.debug "Rider Is Home; Send A Text" + sendText() + } + +} + +def riderIsHome() { + +// log.debug "rider presence: ${rider.currentPresence}" + + if (rider.currentPresence != "present") + { + return false + } + + def riderState = rider.currentState("presence") +// log.debug "riderState: ${riderState}" + if (!riderState) + { + return true + } + + def latestState = rider.latestState("presence") + + def now = new Date() + def minusFive = new Date(minutes: now.minutes - 5) + + + if (minusFive > latestState.date) + { + return true + } + + return false +} + +def sendText() { + if (location.contactBookEnabled) { + sendNotificationToContacts(message ?: "Your ride is here!", recipients) + } + else { + sendSms(phoneNumber, message ?: "Your ride is here!") + } +} diff --git a/smartapps/smartthings/close-the-valve.src/close-the-valve.groovy b/smartapps/smartthings/close-the-valve.src/close-the-valve.groovy new file mode 100644 index 00000000000..0e6cc6ff4e4 --- /dev/null +++ b/smartapps/smartthings/close-the-valve.src/close-the-valve.groovy @@ -0,0 +1,87 @@ +/** + * Close a valve if moisture is detected + * + * Copyright 2014 SmartThings + * + * Author: Juan Risso + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Close The Valve", + namespace: "smartthings", + author: "SmartThings", + description: "Close a selected valve if moisture is detected, and get notified by SMS and push notification.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png" +) + +preferences { + section("When water is sensed...") { + input "sensor", "capability.waterSensor", title: "Where?", required: true, multiple: true + } + section("Close the valve...") { + input "valve", "capability.valve", title: "Which?", required: true, multiple: false + } + section("Send this message (optional, sends standard status message if not specified)"){ + input "messageText", "text", title: "Message Text", required: false + } + section("Via a push notification and/or an SMS message"){ + input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes","No"] + } + section("Minimum time between messages (optional)") { + input "frequency", "decimal", title: "Minutes", required: false + } +} + +def installed() { + subscribe(sensor, "water", waterHandler) +} + +def updated() { + unsubscribe() + subscribe(sensor, "water", waterHandler) +} + +def waterHandler(evt) { + log.debug "Sensor says ${evt.value}" + if (evt.value == "wet") { + valve.close() + } + if (frequency) { + def lastTime = state[evt.deviceId] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + sendMessage(evt) + } + } + else { + sendMessage(evt) + } +} + +private sendMessage(evt) { + def msg = messageText ?: "We closed the valve because moisture was detected" + log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" + + if (!phone || pushAndPhone != "No") { + log.debug "sending push" + sendPush(msg) + } + if (phone) { + log.debug "sending SMS" + sendSms(phone, msg) + } + if (frequency) { + state[evt.deviceId] = now() + } +} \ No newline at end of file diff --git a/smartapps/smartthings/curling-iron.src/curling-iron.groovy b/smartapps/smartthings/curling-iron.src/curling-iron.groovy new file mode 100644 index 00000000000..8f9203fc432 --- /dev/null +++ b/smartapps/smartthings/curling-iron.src/curling-iron.groovy @@ -0,0 +1,114 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Curling Iron + * + * Author: SmartThings + * Date: 2013-03-20 + */ +definition( + name: "Curling Iron", + namespace: "smartthings", + author: "SmartThings", + description: "Turns on an outlet when the user is present and off after a period of time", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png" +) + +preferences { + section("When someone's around because of...") { + input name: "motionSensors", title: "Motion here", type: "capability.motionSensor", multiple: true, required: false + input name: "presenceSensors", title: "And (optionally) these sensors being present", type: "capability.presenceSensor", multiple: true, required: false + } + section("Turn on these outlet(s)") { + input name: "outlets", title: "Which?", type: "capability.switch", multiple: true + } + section("For this amount of time") { + input name: "minutes", title: "Minutes?", type: "number", multiple: false + } +} + +def installed() { + subscribeToEvents() +} + +def updated() { + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(motionSensors, "motion.active", motionActive) + subscribe(motionSensors, "motion.inactive", motionInactive) + subscribe(presenceSensors, "presence.not present", notPresent) +} + +def motionActive(evt) { + log.debug "$evt.name: $evt.value" + if (anyHere()) { + outletsOn() + } +} + +def motionInactive(evt) { + log.debug "$evt.name: $evt.value" + if (allQuiet()) { + outletsOff() + } +} + +def notPresent(evt) { + log.debug "$evt.name: $evt.value" + if (!anyHere()) { + outletsOff() + } +} + +def allQuiet() { + def result = true + for (it in motionSensors) { + if (it.currentMotion == "active") { + result = false + break + } + } + return result +} + +def anyHere() { + def result = true + for (it in presenceSensors) { + if (it.currentPresence == "not present") { + result = false + break + } + } + return result +} + +def outletsOn() { + outlets.on() + unschedule("scheduledTurnOff") +} + +def outletsOff() { + def delay = minutes * 60 + runIn(delay, "scheduledTurnOff") +} + +def scheduledTurnOff() { + outlets.off() + unschedule("scheduledTurnOff") // Temporary work-around to scheduling bug +} + + diff --git a/smartapps/smartthings/darken-behind-me.src/darken-behind-me.groovy b/smartapps/smartthings/darken-behind-me.src/darken-behind-me.groovy new file mode 100644 index 00000000000..e5576bf2d2f --- /dev/null +++ b/smartapps/smartthings/darken-behind-me.src/darken-behind-me.groovy @@ -0,0 +1,49 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Darken Behind Me + * + * Author: SmartThings + */ +definition( + name: "Darken Behind Me", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights off after a period of no motion being observed.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet@2x.png" +) + +preferences { + section("When there's no movement...") { + input "motion1", "capability.motionSensor", title: "Where?" + } + section("Turn off a light...") { + input "switch1", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(motion1, "motion.inactive", motionInactiveHandler) +} + +def updated() +{ + unsubscribe() + subscribe(motion1, "motion.inactive", motionInactiveHandler) +} + +def motionInactiveHandler(evt) { + switch1.off() +} diff --git a/smartapps/smartthings/double-tap.src/double-tap.groovy b/smartapps/smartthings/double-tap.src/double-tap.groovy new file mode 100644 index 00000000000..596d4941b44 --- /dev/null +++ b/smartapps/smartthings/double-tap.src/double-tap.groovy @@ -0,0 +1,100 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Double Tap + * + * Author: SmartThings + */ +definition( + name: "Double Tap", + namespace: "smartthings", + author: "SmartThings", + description: "Turn on or off any number of switches when an existing switch is tapped twice in a row.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section("When this switch is double-tapped...") { + input "master", "capability.switch", title: "Where?" + } + section("Turn on or off all of these switches as well") { + input "switches", "capability.switch", multiple: true, required: false + } + section("And turn off but not on all of these switches") { + input "offSwitches", "capability.switch", multiple: true, required: false + } + section("And turn on but not off all of these switches") { + input "onSwitches", "capability.switch", multiple: true, required: false + } +} + +def installed() +{ + subscribe(master, "switch", switchHandler, [filterEvents: false]) +} + +def updated() +{ + unsubscribe() + subscribe(master, "switch", switchHandler, [filterEvents: false]) +} + +def switchHandler(evt) { + log.info evt.value + + // use Event rather than DeviceState because we may be changing DeviceState to only store changed values + def recentStates = master.eventsSince(new Date(now() - 4000), [all:true, max: 10]).findAll{it.name == "switch"} + log.debug "${recentStates?.size()} STATES FOUND, LAST AT ${recentStates ? recentStates[0].dateCreated : ''}" + + if (evt.physical) { + if (evt.value == "on" && lastTwoStatesWere("on", recentStates, evt)) { + log.debug "detected two taps, turn on other light(s)" + onSwitches()*.on() + } else if (evt.value == "off" && lastTwoStatesWere("off", recentStates, evt)) { + log.debug "detected two taps, turn off other light(s)" + offSwitches()*.off() + } + } + else { + log.trace "Skipping digital on/off event" + } +} + +private onSwitches() { + (switches + onSwitches).findAll{it} +} + +private offSwitches() { + (switches + offSwitches).findAll{it} +} + +private lastTwoStatesWere(value, states, evt) { + def result = false + if (states) { + + log.trace "unfiltered: [${states.collect{it.dateCreated + ':' + it.value}.join(', ')}]" + def onOff = states.findAll { it.physical || !it.type } + log.trace "filtered: [${onOff.collect{it.dateCreated + ':' + it.value}.join(', ')}]" + + // This test was needed before the change to use Event rather than DeviceState. It should never pass now. + if (onOff[0].date.before(evt.date)) { + log.warn "Last state does not reflect current event, evt.date: ${evt.dateCreated}, state.date: ${onOff[0].dateCreated}" + result = evt.value == value && onOff[0].value == value + } + else { + result = onOff.size() > 1 && onOff[0].value == value && onOff[1].value == value + } + } + result +} diff --git a/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy b/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy new file mode 100644 index 00000000000..17cf709603f --- /dev/null +++ b/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy @@ -0,0 +1,55 @@ +/** + * Dry the Wetspot + * + * Copyright 2014 Scottin Pollock + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Dry the Wetspot", + namespace: "smartthings", + author: "Scottin Pollock", + description: "Turns switch on and off based on moisture sensor input.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png" +) + + +preferences { + section("When water is sensed...") { + input "sensor", "capability.waterSensor", title: "Where?", required: true + } + section("Turn on a pump...") { + input "pump", "capability.switch", title: "Which?", required: true + } +} + +def installed() { + subscribe(sensor, "water.dry", waterHandler) + subscribe(sensor, "water.wet", waterHandler) +} + +def updated() { + unsubscribe() + subscribe(sensor, "water.dry", waterHandler) + subscribe(sensor, "water.wet", waterHandler) +} + +def waterHandler(evt) { + log.debug "Sensor says ${evt.value}" + if (evt.value == "wet") { + pump.on() + } else if (evt.value == "dry") { + pump.off() + } +} + diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy new file mode 100644 index 00000000000..12b703d0b55 --- /dev/null +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -0,0 +1,919 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Ecobee Service Manager + * + * Author: scott + * Date: 2013-08-07 + * + * Last Modification: + * JLH - 01-23-2014 - Update for Correct SmartApp URL Format + * JLH - 02-15-2014 - Fuller use of ecobee API + */ +definition( + name: "Ecobee (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Connect your Ecobee thermostat to SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png" +) { + appSetting "clientId" + appSetting "serverUrl" +} + +preferences { + page(name: "auth", title: "ecobee", nextPage:"deviceList", content:"authPage", uninstall: true) + page(name: "deviceList", title: "ecobee", content:"ecobeeDeviceList", install:true) +} + +mappings { + path("/auth") { + action: [ + GET: "auth" + ] + } + path("/swapToken") { + action: [ + GET: "swapToken" + ] + } +} + +def auth() { + redirect location: oauthInitUrl() +} + +def authPage() +{ + log.debug "authPage()" + + if(!atomicState.accessToken) + { + log.debug "about to create access token" + createAccessToken() + atomicState.accessToken = state.accessToken + } + + + def description = "Required" + def uninstallAllowed = false + def oauthTokenProvided = false + + if(atomicState.authToken) + { + // TODO: Check if it's valid + if(true) + { + description = "You are connected." + uninstallAllowed = true + oauthTokenProvided = true + } + else + { + description = "Required" // Worth differentiating here vs. not having atomicState.authToken? + oauthTokenProvided = false + } + } + + def redirectUrl = buildRedirectUrl("auth") + + log.debug "RedirectUrl = ${redirectUrl}" + + // get rid of next button until the user is actually auth'd + + if (!oauthTokenProvided) { + + return dynamicPage(name: "auth", title: "Login", nextPage:null, uninstall:uninstallAllowed) { + section(){ + paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button." + href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description + } + } + + } else { + + return dynamicPage(name: "auth", title: "Log In", nextPage:"deviceList", uninstall:uninstallAllowed) { + section(){ + paragraph "Tap Next to continue to setup your thermostats." + href url:redirectUrl, style:"embedded", state:"complete", title:"ecobee", description:description + } + } + + } + +} + +def ecobeeDeviceList() +{ + log.debug "ecobeeDeviceList()" + + def stats = getEcobeeThermostats() + + log.debug "device list: $stats" + + def p = dynamicPage(name: "deviceList", title: "Select Your Thermostats", uninstall: true) { + section(""){ + paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings." + input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats]) + } + } + + log.debug "list p: $p" + return p +} + +def getEcobeeThermostats() +{ + log.debug "getting device list" + + def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true}}' + + def deviceListParams = [ + uri: "https://api.ecobee.com", + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: requestBody] + ] + + log.debug "_______AUTH______ ${atomicState.authToken}" + log.debug "device list params: $deviceListParams" + + def stats = [:] + httpGet(deviceListParams) { resp -> + + if(resp.status == 200) + { + resp.data.thermostatList.each { stat -> + def dni = [ app.id, stat.identifier ].join('.') + stats[dni] = getThermostatDisplayName(stat) + } + } + else + { + log.debug "http status: ${resp.status}" + + //refresh the auth token + if (resp.status == 500 && resp.data.status.code == 14) + { + log.debug "Storing the failed action to try later" + atomicState.action = "getEcobeeThermostats" + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } + else + { + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + } + } + } + + log.debug "thermostats: $stats" + + return stats +} + +def getThermostatDisplayName(stat) +{ + log.debug "getThermostatDisplayName" + if(stat?.name) + { + return stat.name.toString() + } + + return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() +} + +def getThermostatTypeName(stat) +{ + log.debug "getThermostatTypeName" + return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart" +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + // createAccessToken() + + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + // TODO: subscribe to attributes, devices, locations, etc. + log.debug "initialize" + def devices = thermostats.collect { dni -> + + def d = getChildDevice(dni) + + if(!d) + { + d = addChildDevice(getChildNamespace(), getChildName(), dni) + log.debug "created ${d.displayName} with id $dni" + } + else + { + log.debug "found ${d.displayName} with id $dni already exists" + } + + return d + } + + log.debug "created ${devices.size()} thermostats" + + def delete + // Delete any that are no longer in settings + if(!thermostats) + { + log.debug "delete thermostats" + delete = getAllChildDevices() + } + else + { + delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) } + } + + log.debug "deleting ${delete.size()} thermostats" + delete.each { deleteChildDevice(it.deviceNetworkId) } + + atomicState.thermostatData = [:] + + pollHandler() + + // schedule ("0 0/15 * 1/1 * ? *", pollHandler) +} + + +def oauthInitUrl() +{ + log.debug "oauthInitUrl" + // def oauth_url = "https://api.ecobee.com/authorize?response_type=code&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=http://localhost/&scope=smartRead,smartWrite&state=abc123" + def stcid = getSmartThingsClientId(); + + atomicState.oauthInitState = UUID.randomUUID().toString() + + def oauthParams = [ + response_type: "code", + scope: "smartRead,smartWrite", + client_id: stcid, + state: atomicState.oauthInitState, + redirect_uri: buildRedirectUrl() + ] + + return "https://api.ecobee.com/authorize?" + toQueryString(oauthParams) +} + +def buildRedirectUrl(action = "swapToken") +{ + log.debug "buildRedirectUrl" + // return serverUrl + "/api/smartapps/installations/${app.id}/token/${atomicState.accessToken}" + return serverUrl + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/${action}" +} + +def swapToken() +{ + log.debug "swapping token: $params" + debugEvent ("swapping token: $params") + + def code = params.code + def oauthState = params.state + + // TODO: verify oauthState == atomicState.oauthInitState + + + + // https://www.ecobee.com/home/token?grant_type=authorization_code&code=aliOpagDm3BqbRplugcs1AwdJE0ohxdB&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=https://graph.api.smartthings.com/ + def stcid = getSmartThingsClientId() + + def tokenParams = [ + grant_type: "authorization_code", + code: params.code, + client_id: stcid, + redirect_uri: buildRedirectUrl() + ] + + def tokenUrl = "https://www.ecobee.com/home/token?" + toQueryString(tokenParams) + + log.debug "SCOTT: swapping token $params" + + def jsonMap + httpPost(uri:tokenUrl) { resp -> + jsonMap = resp.data + } + + log.debug "SCOTT: swapped token for $jsonMap" + debugEvent ("swapped token for $jsonMap") + + atomicState.refreshToken = jsonMap.refresh_token + atomicState.authToken = jsonMap.access_token + + def html = """ + + + + +Withings Connection + + + +
+ ecobee icon + connected device icon + SmartThings logo +

Your ecobee Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

+
+ + +""" + + render contentType: 'text/html', data: html +} + +def getPollRateMillis() { return 15 * 60 * 1000 } + +// Poll Child is invoked from the Child Device itself as part of the Poll Capability +def pollChild( child ) +{ + log.debug "poll child" + debugEvent ("poll child") + def now = new Date().time + + debugEvent ("Last Poll Millis = ${atomicState.lastPollMillis}") + def last = atomicState.lastPollMillis ?: 0 + def next = last + pollRateMillis + + log.debug "pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}" + debugEvent ("pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}") + + // if( now > next ) + if( true ) // for now let's always poll/refresh + { + log.debug "polling children because $now > $next" + debugEvent("polling children because $now > $next") + + pollChildren() + + log.debug "polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}" + debugEvent ("polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}") + + def currentTime = new Date().time + debugEvent ("Current Time = ${currentTime}") + atomicState.lastPollMillis = currentTime + + def tData = atomicState.thermostats[child.device.deviceNetworkId] + + if(!tData) + { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + + // TODO: flag device as in error state + // child.errorState = true + + return null + } + + tData.updated = currentTime + + return tData.data + } + else if(atomicState.thermostats[child.device.deviceNetworkId] != null) + { + log.debug "not polling children, found child ${child.device.deviceNetworkId} " + + def tData = atomicState.thermostats[child.device.deviceNetworkId] + if(!tData.updated) + { + // we have pulled new data for this thermostat, but it has not asked us for it + // track it and return the data + tData.updated = new Date().time + return tData.data + } + return null + } + else if(atomicState.thermostats[child.device.deviceNetworkId] == null) + { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" + + // TODO: flag device as in error state + // child.errorState = true + + return null + } + else + { + // it's not time to poll again and this thermostat already has its latest values + } + + return null +} + +def availableModes(child) +{ + + debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") + + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") + + def tData = atomicState.thermostats[child.device.deviceNetworkId] + + debugEvent("Data = ${tData}") + + if(!tData) + { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + + // TODO: flag device as in error state + // child.errorState = true + + return null + } + + def modes = ["off"] + + if (tData.data.heatMode) modes.add("heat") + if (tData.data.coolMode) modes.add("cool") + if (tData.data.autoMode) modes.add("auto") + if (tData.data.auxHeatMode) modes.add("auxHeatOnly") + + modes + +} + + +def currentMode(child) +{ + + debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") + + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") + + def tData = atomicState.thermostats[child.device.deviceNetworkId] + + debugEvent("Data = ${tData}") + + if(!tData) + { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + + // TODO: flag device as in error state + // child.errorState = true + + return null + } + + def mode = tData.data.thermostatMode + + mode + +} + + + +def pollChildren() +{ + log.debug "polling children" + def thermostatIdsString = getChildDeviceIdsString() + + log.debug "polling children: $thermostatIdsString" + + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true"}}' + + // // TODO: test this: + // + // def jsonRequestBody = toJson([ + // selection:[ + // selectionType: "thermostats", + // selectionMatch: getChildDeviceIdsString(), + // includeRuntime: true + // ] + // ]) + log.debug "json Request: " + jsonRequestBody + + log.debug "State AuthToken: ${atomicState.authToken}" + debugEvent "State AuthToken: ${atomicState.authToken}" + + + def pollParams = [ + uri: "https://api.ecobee.com", + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: jsonRequestBody] + ] + + debugEvent ("Before HTTPGET to ecobee.") + + try{ + httpGet(pollParams) { resp -> + + if (resp.data) { + debugEvent ("Response from ecobee GET = ${resp.data}") + debugEvent ("Response Status = ${resp.status}") + } + + if(resp.status == 200) { + log.debug "poll results returned" + + atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat -> + + def dni = [ app.id, stat.identifier ].join('.') + + log.debug "updating dni $dni" + + def data = [ + coolMode: (stat.settings.coolStages > 0), + heatMode: (stat.settings.heatStages > 0), + autoMode: stat.settings.autoHeatCoolFeatureEnabled, + auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), + temperature: stat.runtime.actualTemperature / 10, + heatingSetpoint: stat.runtime.desiredHeat / 10, + coolingSetpoint: stat.runtime.desiredCool / 10, + thermostatMode: stat.settings.hvacMode + ] + + debugEvent ("Event Data = ${data}") + + collector[dni] = [data:data] + return collector + } + + log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" + } + else + { + log.error "polling children & got http status ${resp.status}" + + //refresh the auth token + if (resp.status == 500 && resp.data.status.code == 14) + { + log.debug "Storing the failed action to try later" + atomicState.action = "pollChildren"; + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } + else + { + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + } + } + } + } + catch(Exception e) + { + log.debug "___exception polling children: " + e + debugEvent ("${e}") + + refreshAuthToken() + } + +} + +def pollHandler() { + + debugEvent ("in Poll() method.") + pollChildren() // Hit the ecobee API for update on all thermostats + + atomicState.thermostats.each {stat -> + + def dni = stat.key + + log.debug ("DNI = ${dni}") + debugEvent ("DNI = ${dni}") + + def d = getChildDevice(dni) + + if(d) + { + log.debug ("Found Child Device.") + debugEvent ("Found Child Device.") + debugEvent("Event Data before generate event call = ${stat}") + + d.generateEvent(atomicState.thermostats[dni].data) + + } + + } + +} + +def getChildDeviceIdsString() +{ + return thermostats.collect { it.split(/\./).last() }.join(',') +} + +def toJson(Map m) +{ + return new org.json.JSONObject(m).toString() +} + +def toQueryString(Map m) +{ + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +private refreshAuthToken() { + log.debug "refreshing auth token" + debugEvent("refreshing OAUTH token") + + if(!atomicState.refreshToken) { + log.warn "Can not refresh OAuth token since there is no refreshToken stored" + } else { + def stcid = getSmartThingsClientId() + + def refreshParams = [ + method: 'POST', + uri : "https://api.ecobee.com", + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: stcid], + + //data?.refreshToken + ] + + log.debug refreshParams + + //changed to httpPost + try { + def jsonMap + httpPost(refreshParams) { resp -> + + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" + + debugEvent("Token refreshed ... calling saved RestAction now!") + + log.debug resp + + jsonMap = resp.data + + if(resp.data) { + + log.debug resp.data + debugEvent("Response = ${resp.data}") + + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token + + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") + + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" + + "{atomicState.action}"() + + //remove saved action + atomicState.action = "" + } + + } + atomicState.action = "" + } else { + log.debug "refresh failed ${resp.status} : ${resp.status.code}" + } + } + + // atomicState.refreshToken = jsonMap.refresh_token + // atomicState.authToken = jsonMap.access_token + } + catch(Exception e) { + log.debug "caught exception refreshing auth token: " + e + } + } +} + +def resumeProgram(child) +{ + + def thermostatIdsString = getChildDeviceIdsString() + log.debug "resumeProgram children: $thermostatIdsString" + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' + //, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } } + sendJson(jsonRequestBody) +} + +def setHold(child, heating, cooling) +{ + + int h = heating * 10 + int c = cooling * 10 + + log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c" + def thermostatIdsString = getChildDeviceIdsString() + log.debug "setCoolingSetpoint children: $thermostatIdsString" + + + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' + +// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' + + sendJson(jsonRequestBody) +} + +def setMode(child, mode) +{ + log.debug "requested mode = ${mode}" + def thermostatIdsString = getChildDeviceIdsString() + log.debug "setCoolingSetpoint children: $thermostatIdsString" + + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' + + log.debug "Mode Request Body = ${jsonRequestBody}" + debugEvent ("Mode Request Body = ${jsonRequestBody}") + + def result = sendJson(jsonRequestBody) + + if (result) { + def tData = atomicState.thermostats[child.device.deviceNetworkId] + tData.data.thermostatMode = mode + } + + return(result) +} + +def changeSetpoint (child, amount) +{ + def tData = atomicState.thermostats[child.device.deviceNetworkId] + + log.debug "In changeSetpoint." + debugEvent ("In changeSetpoint.") + + if (tData) { + + def thermostat = tData.data + + log.debug "Thermostat=${thermostat}" + debugEvent ("Thermostat=${thermostat}") + + if (thermostat.thermostatMode == "heat") { + thermostat.heatingSetpoint = thermostat.heatingSetpoint + amount + child.setHeatingSetpoint (thermostat.heatingSetpoint) + + log.debug "New Heating Setpoint = ${thermostat.heatingSetpoint}" + debugEvent ("New Heating Setpoint = ${thermostat.heatingSetpoint}") + + } + else if (thermostat.thermostatMode == "cool") { + thermostat.coolingSetpoint = thermostat.coolingSetpoint + amount + child.setCoolingSetpoint (thermostat.coolingSetpoint) + + log.debug "New Cooling Setpoint = ${thermostat.coolingSetpoint}" + debugEvent ("New Cooling Setpoint = ${thermostat.coolingSetpoint}") + } + } +} + + +def sendJson(String jsonBody) +{ + + //log.debug "_____AUTH_____ ${atomicState.authToken}" + + def cmdParams = [ + uri: "https://api.ecobee.com", + + path: "/1/thermostat", + headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + body: jsonBody + ] + + def returnStatus = -1 + + try{ + httpPost(cmdParams) { resp -> + + if(resp.status == 200) { + + log.debug "updated ${resp.data}" + debugEvent("updated ${resp.data}") + returnStatus = resp.data.status.code + if (resp.data.status.code == 0) + log.debug "Successful call to ecobee API." + else { + log.debug "Error return code = ${resp.data.status.code}" + debugEvent("Error return code = ${resp.data.status.code}") + } + } + else + { + log.error "sent Json & got http status ${resp.status} - ${resp.status.code}" + debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}") + + //refresh the auth token + if (resp.status == 500 && resp.status.code == 14) + { + //log.debug "Storing the failed action to try later" + log.debug "Refreshing your auth_token!" + debugEvent ("Refreshing OAUTH Token") + refreshAuthToken() + return false + } + else + { + debugEvent ("Authentication error, invalid authentication method, lack of credentials, etc.") + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + return false + } + } + } + } + catch(Exception e) + { + log.debug "Exception Sending Json: " + e + debugEvent ("Exception Sending JSON: " + e) + return false + } + + if (returnStatus == 0) + return true + else + return false +} + + +def getChildNamespace() { "smartthings" } +def getChildName() { "Ecobee Thermostat" } + +def getServerUrl() { return appSettings.serverUrl } +def getSmartThingsClientId() { appSettings.clientId } + +def debugEvent(message, displayEvent = false) { + + def results = [ + name: "appdebug", + descriptionText: message, + displayed: displayEvent + ] + log.debug "Generating AppDebug Event: ${results}" + sendEvent (results) + +} diff --git a/smartapps/smartthings/elder-care-daily-routine.src/elder-care-daily-routine.groovy b/smartapps/smartthings/elder-care-daily-routine.src/elder-care-daily-routine.groovy new file mode 100644 index 00000000000..e0324206814 --- /dev/null +++ b/smartapps/smartthings/elder-care-daily-routine.src/elder-care-daily-routine.groovy @@ -0,0 +1,143 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Elder Care + * + * Author: SmartThings + * Date: 2013-03-06 + * + * Stay connected to your loved ones. Get notified if they are not up and moving around + * by a specified time and/or if they have not opened a cabinet or door according to a set schedule. + */ + +definition( + name: "Elder Care: Daily Routine", + namespace: "smartthings", + author: "SmartThings", + description: "Stay connected to your loved ones. Get notified if they are not up and moving around by a specified time and/or if they have not opened a cabinet or door according to a set schedule.", + category: "Family", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png" +) + +preferences { + section("Who are you checking on?") { + input "person1", "text", title: "Name?" + } + section("If there's no movement (optional, leave blank to not require)...") { + input "motion1", "capability.motionSensor", title: "Where?", required: false + } + section("or a door or cabinet hasn't been opened (optional, leave blank to not require)...") { + input "contact1", "capability.contactSensor", required: false + } + section("between these times...") { + input "time0", "time", title: "From what time?" + input "time1", "time", title: "Until what time?" + } + section("then alert the following people...") { + input("recipients", "contact", title: "People to notify", description: "Send notifications to") { + input "phone1", "phone", title: "Phone number?", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + schedule(time1, "scheduleCheck") +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() //TODO no longer subscribe like we used to - clean this up after all apps updated + unschedule() + schedule(time1, "scheduleCheck") +} + +def scheduleCheck() +{ + if(noRecentContact() && noRecentMotion()) { + def person = person1 ?: "your elder" + def msg = "Alert! There has been no activity at ${person}'s place ${timePhrase}" + log.debug msg + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (phone1) { + sendSms(phone1, msg) + } else { + sendPush(msg) + } + } + } else { + log.debug "There has been activity ${timePhrase}, not sending alert" + } +} + +private noRecentMotion() +{ + if(motion1) { + def motionEvents = motion1.eventsSince(sinceTime) + log.trace "Found ${motionEvents?.size() ?: 0} motion events" + if (motionEvents.find { it.value == "active" }) { + log.debug "There have been recent 'active' events" + return false + } else { + log.debug "There have not been any recent 'active' events" + return true + } + } else { + log.debug "Motion sensor not enabled" + return true + } +} + +private noRecentContact() +{ + if(contact1) { + def contactEvents = contact1.eventsSince(sinceTime) + log.trace "Found ${contactEvents?.size() ?: 0} door events" + if (contactEvents.find { it.value == "open" }) { + log.debug "There have been recent 'open' events" + return false + } else { + log.debug "There have not been any recent 'open' events" + return true + } + } else { + log.debug "Contact sensor not enabled" + return true + } +} + +private getSinceTime() { + if (time0) { + return timeToday(time0, location?.timeZone) + } + else { + return new Date(now() - 21600000) + } +} + +private getTimePhrase() { + def interval = now() - sinceTime.time + if (interval < 3600000) { + return "in the past ${Math.round(interval/60000)} minutes" + } + else if (interval < 7200000) { + return "in the past hour" + } + else { + return "in the past ${Math.round(interval/3600000)} hours" + } +} diff --git a/smartapps/smartthings/elder-care-slip-fall.src/elder-care-slip-fall.groovy b/smartapps/smartthings/elder-care-slip-fall.src/elder-care-slip-fall.groovy new file mode 100644 index 00000000000..2c893ae34fd --- /dev/null +++ b/smartapps/smartthings/elder-care-slip-fall.src/elder-care-slip-fall.groovy @@ -0,0 +1,124 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Elder Care: Slip & Fall + * + * Author: SmartThings + * Date: 2013-04-07 + * + */ + +definition( + name: "Elder Care: Slip & Fall", + namespace: "smartthings", + author: "SmartThings", + description: "Monitors motion sensors in bedroom and bathroom during the night and detects if occupant does not return from the bathroom after a specified period of time.", + category: "Family", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png" +) + +preferences { + section("Bedroom motion detector(s)") { + input "bedroomMotion", "capability.motionSensor", multiple: true + } + section("Bathroom motion detector") { + input "bathroomMotion", "capability.motionSensor" + } + section("Active between these times") { + input "startTime", "time", title: "Start Time" + input "stopTime", "time", title: "Stop Time" + } + section("Send message when no return within specified time period") { + input "warnMessage", "text", title: "Warning Message" + input "threshold", "number", title: "Minutes" + } + section("To these contacts") { + input("recipients", "contact", title: "Recipients", description: "Send notifications to") { + input "phone1", "phone", required: false + input "phone2", "phone", required: false + input "phone3", "phone", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + state.active = 0 + subscribe(bedroomMotion, "motion.active", bedroomActive) + subscribe(bathroomMotion, "motion.active", bathroomActive) +} + +def bedroomActive(evt) { + def start = timeToday(startTime, location?.timeZone) + def stop = timeToday(stopTime, location?.timeZone) + def now = new Date() + log.debug "bedroomActive, status: $state.ststus, start: $start, stop: $stop, now: $now" + if (state.status == "waiting") { + log.debug "motion detected in bedroom, disarming" + unschedule("sendMessage") + state.status = null + } + else { + if (start.before(now) && stop.after(now)) { + log.debug "motion in bedroom, look for bathroom motion" + state.status = "pending" + } + else { + log.debug "Not in time window" + } + } +} + +def bathroomActive(evt) { + log.debug "bathroomActive, status: $state.status" + if (state.status == "pending") { + def delay = threshold.toInteger() * 60 + state.status = "waiting" + log.debug "runIn($delay)" + runIn(delay, sendMessage) + } +} + +def sendMessage() { + log.debug "sendMessage" + def msg = warnMessage + log.info msg + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush msg + if (phone1) { + sendSms phone1, msg + } + if (phone2) { + sendSms phone2, msg + } + if (phone3) { + sendSms phone3, msg + } + } + state.status = null +} diff --git a/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy b/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy new file mode 100644 index 00000000000..2aad9bbd1d0 --- /dev/null +++ b/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy @@ -0,0 +1,101 @@ +/** + * Energy Saver + * + * Copyright 2014 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. + * + */ +definition( + name: "Energy Alerts", + namespace: "smartthings", + author: "SmartThings", + description: "Get notified if you're using too much energy", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png" +) + +preferences { + section { + input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false, description: null) + input(name: "aboveThreshold", type: "number", title: "Reports Above...", required: true, description: "in either watts or kw.") + input(name: "belowThreshold", type: "number", title: "Or Reports Below...", required: true, description: "in either watts or kw.") + } + section { + input("recipients", "contact", title: "Send notifications to") { + input(name: "sms", type: "phone", title: "Send A Text To", description: null, required: false) + input(name: "pushNotification", type: "bool", title: "Send a push notification", description: null, defaultValue: true) + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe(meter, "power", meterHandler) +} + +def meterHandler(evt) { + + def meterValue = evt.value as double + + if (!atomicState.lastValue) { + atomicState.lastValue = meterValue + } + + def lastValue = atomicState.lastValue as double + atomicState.lastValue = meterValue + + def aboveThresholdValue = aboveThreshold as int + if (meterValue > aboveThresholdValue) { + if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold + def msg = "${meter} reported ${evt.value} ${evt.unit} which is above your threshold of ${aboveThreshold}." + sendMessage(msg) + } else { +// log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed" + } + } + + + def belowThresholdValue = belowThreshold as int + if (meterValue < belowThresholdValue) { + if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold + def msg = "${meter} reported ${evt.value} ${evt.unit} which is below your threshold of ${belowThreshold}." + sendMessage(msg) + } else { +// log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed" + } + } +} + +def sendMessage(msg) { + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (sms) { + sendSms(sms, msg) + } + if (pushNotification) { + sendPush(msg) + } + } +} diff --git a/smartapps/smartthings/energy-saver.src/energy-saver.groovy b/smartapps/smartthings/energy-saver.src/energy-saver.groovy new file mode 100644 index 00000000000..7f7d536e4b1 --- /dev/null +++ b/smartapps/smartthings/energy-saver.src/energy-saver.groovy @@ -0,0 +1,59 @@ +/** + * Energy Saver + * + * Copyright 2014 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. + * + */ +definition( + name: "Energy Saver", + namespace: "smartthings", + author: "SmartThings", + description: "Turn things off if you're using too much energy", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section { + input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false, description: null) + input(name: "threshold", type: "number", title: "Reports Above...", required: true, description: "in either watts or kw.") + } + section { + input(name: "switches", type: "capability.switch", title: "Turn Off These Switches", required: true, multiple: true, description: null) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe(meter, "power", meterHandler) +} + +def meterHandler(evt) { + def meterValue = evt.value as double + def thresholdValue = threshold as int + if (meterValue > thresholdValue) { + log.debug "${meter} reported energy consumption above ${threshold}. Turning of switches." + switches.off() + } +} diff --git a/smartapps/smartthings/examples/every-element.src/every-element.groovy b/smartapps/smartthings/examples/every-element.src/every-element.groovy new file mode 100644 index 00000000000..82ff69a53c5 --- /dev/null +++ b/smartapps/smartthings/examples/every-element.src/every-element.groovy @@ -0,0 +1,362 @@ +/** + * Every Element + * + * Copyright 2014 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. + * + */ +definition( + name: "Every Element", + namespace: "smartthings/examples", + author: "SmartThings", + description: "Every element demonstration app", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" +) + +preferences { + page(name: "firstPage") + page(name: "inputPage") + page(name: "appPage") + page(name: "labelPage") + page(name: "modePage") + page(name: "paragraphPage") + page(name: "iconPage") + page(name: "hrefPage") + page(name: "buttonsPage") + page(name: "imagePage") + page(name: "videoPage") + page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo") + page(name: "flattenedPage") +} + +def firstPage() { + dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) { + section() { + href(page: "inputPage", title: "Element: 'input'") + href(page: "appPage", title: "Element: 'app'") + href(page: "labelPage", title: "Element: 'label'") + href(page: "modePage", title: "Element: 'mode'") + href(page: "paragraphPage", title: "Element: 'paragraph'") + href(page: "iconPage", title: "Element: 'icon'") + href(page: "hrefPage", title: "Element: 'href'") + href(page: "buttonsPage", title: "Element: 'buttons'") + href(page: "imagePage", title: "Element: 'image'") + href(page: "videoPage", title: "Element: 'video'") + } + section() { + href(page: "flattenedPage", title: "All of the above elements on a single page") + } + } +} + +def inputPage() { + dynamicPage(name: "inputPage", title: "Every 'input' type") { + section("enum") { + input(type: "enum", name: "enumRefresh", title: "submitOnChange:true", required: false, multiple: true, options: ["one", "two", "three"], submitOnChange: true) + if (enumRefresh) { + paragraph "${enumRefresh}" + } + input(type: "enum", name: "enumSegmented", title: "style:segmented", required: false, multiple: true, options: ["one", "two", "three"], style: "segmented") + input(type: "enum", name: "enum", title: "required:false, multiple:false", required: false, multiple: false, options: ["one", "two", "three"]) + input(type: "enum", name: "enumRequired", title: "required:true", required: true, multiple: false, options: ["one", "two", "three"]) + input(type: "enum", name: "enumMultiple", title: "multiple:true", required: false, multiple: true, options: ["one", "two", "three"]) + input(type: "enum", name: "enumWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, options: ["one", "two", "three"], image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + input(type: "enum", name: "enumWithGroupedOptions", title: "groupedOptions", description: "This enum has grouped options", required: false, multiple: true, groupedOptions: [ + [ + title : "the group title that is displayed", + order : 0, // the order of the group; 0-based + image : "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", // not yet supported + values: [ + [ + key : "the value that will be placed in SmartApp settings.", // such as a device id + value: "the title of the selectable option that is displayed", // such as a device name + order: 0 // the order of the option + ] + ] + ], + [ + title : "the second group title that is displayed", + order : 1, // the order of the group; 0-based + image : null, // not yet supported + values: [ + [ + key : "some_device_id", + value: "some_device_name", + order: 1 // the order of the option. This option will appear second in the list even though it is the first option defined in this map + ], + [ + key : "some_other_device_id", + value: "some_other_device_name", + order: 0 // the order of the option. This option will appear first in the list even though it is not the first option defined in this map + ] + ] + ] + ]) + } + section("text") { + input(type: "text", name: "text", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "text", name: "textRequired", title: "required:true", required: true, multiple: false) + input(type: "text", name: "textWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("number") { + input(type: "number", name: "number", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "number", name: "numberRequired", title: "required:true", required: true, multiple: false) + input(type: "number", name: "numberWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("boolean") { + input(type: "boolean", name: "boolean", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "boolean", name: "booleanWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("password") { + input(type: "password", name: "password", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "password", name: "passwordRequired", title: "required:true", required: true, multiple: false) + input(type: "password", name: "passwordWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("phone") { + input(type: "phone", name: "phone", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "phone", name: "phoneRequired", title: "required:true", required: true, multiple: false) + input(type: "phone", name: "phoneWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("email") { + input(type: "email", name: "email", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "email", name: "emailRequired", title: "required:true", required: true, multiple: false) + input(type: "email", name: "emailWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("decimal") { + input(type: "decimal", name: "decimal", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "decimal", name: "decimalRequired", title: "required:true", required: true, multiple: false) + input(type: "decimal", name: "decimalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("mode") { + input(type: "mode", name: "mode", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "mode", name: "modeRequired", title: "required:true", required: true, multiple: false) + input(type: "mode", name: "modeMultiple", title: "multiple:true", required: false, multiple: true) + input(type: "mode", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("icon") { + input(type: "icon", name: "icon", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "icon", name: "iconRequired", title: "required:true", required: true, multiple: false) + input(type: "icon", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("capability") { + input(type: "capability.switch", name: "capability", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "capability.switch", name: "capabilityRequired", title: "required:true", required: true, multiple: false) + input(type: "capability.switch", name: "capabilityMultiple", title: "multiple:true", required: false, multiple: true) + input(type: "capability.switch", name: "capabilityWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("hub") { + input(type: "hub", name: "hub", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "hub", name: "hubRequired", title: "required:true", required: true, multiple: false) + input(type: "hub", name: "hubMultiple", title: "multiple:true", required: false, multiple: true) + input(type: "hub", name: "hubWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("device") { + input(type: "device.switch", name: "device", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "device.switch", name: "deviceRequired", title: "required:true", required: true, multiple: false) + input(type: "device.switch", name: "deviceMultiple", title: "multiple:true", required: false, multiple: true) + input(type: "device.switch", name: "deviceWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("time") { + input(type: "time", name: "time", title: "required:false, multiple:false", required: false, multiple: false) + input(type: "time", name: "timeRequired", title: "required:true", required: true, multiple: false) + input(type: "time", name: "timeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + section("contact-book") { + input("recipients", "contact", title: "Notify", description: "Send notifications to") { + input(type: "phone", name: "phone", title: "Send text message to", required: false, multiple: false) + input(type: "boolean", name: "boolean", title: "Send push notification", required: false, multiple: false) + } + } + } +} + +def appPage() { + dynamicPage(name: "appPage", title: "Every 'app' type") { + section { + paragraph "These won't work unless you create a child SmartApp to link to... Sorry." + } + section("app") { + app( + name: "app", + title: "required:false, multiple:false", + required: false, + multiple: false, + namespace: "Steve", + appName: "Child SmartApp" + ) + app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp") + app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete") + app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp") + } + section("multiple:true") { + app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp") + } + section("multiple:true with image") { + app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp") + } + } +} + +def labelPage() { + dynamicPage(name: "labelPage", title: "Every 'Label' type") { + section("label") { + label(name: "label", title: "required:false, multiple:false", required: false, multiple: false) + label(name: "labelRequired", title: "required:true", required: true, multiple: false) + label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} + +def modePage() { + dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this + section("mode") { + mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false) + mode(name: "modeRequired", title: "required:true", required: true, multiple: false) + mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true) + mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} + +def paragraphPage() { + dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") { + section("paragraph") { + paragraph "This us how you should make a paragraph element" + paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah." + } + } +} + +def iconPage() { + dynamicPage(name: "iconPage", title: "Every 'icon' type") { // TODO: finish this + section("icon") { + icon(name: "icon", title: "required:false, multiple:false", required: false, multiple: false) + icon(name: "iconRequired", title: "required:true", required: true, multiple: false) + icon(name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} + +def hrefPage() { + dynamicPage(name: "hrefPage", title: "Every 'href' type") { + section("page") { + href(name: "hrefPage", title: "required:false, multiple:false", required: false, multiple: false, page: "deadEnd") + href(name: "hrefPageRequired", title: "required:true", required: true, multiple: false, page: "deadEnd", description: "Don't make hrefs required") + href(name: "hrefPageComplete", title: "state:complete", required: false, multiple: false, page: "deadEnd", state: "complete") + href(name: "hrefPageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", page: "deadEnd",) + } + section("external") { + href(name: "hrefExternal", title: "required:false, multiple:false", required: false, multiple: false, style: "external", url: "http://smartthings.com/") + href(name: "hrefExternalRequired", title: "required:true", required: true, multiple: false, style: "external", url: "http://smartthings.com/", description: "Don't make hrefs required") + href(name: "hrefExternalComplete", title: "state:complete", required: false, multiple: true, style: "external", url: "http://smartthings.com/", state: "complete") + href(name: "hrefExternalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/") + } + section("embedded") { + href(name: "hrefEmbedded", title: "required:false, multiple:false", required: false, multiple: false, style: "embedded", url: "http://smartthings.com/") + href(name: "hrefEmbeddedRequired", title: "required:true", required: true, multiple: false, style: "embedded", url: "http://smartthings.com/", description: "Don't make hrefs required") + href(name: "hrefEmbeddedComplete", title: "state:complete", required: false, multiple: true, style: "embedded", url: "http://smartthings.com/", state: "complete") + href(name: "hrefEmbeddedWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/") + } + } +} + +def buttonsPage() { + dynamicPage(name: "buttonsPage", title: "Every 'button' type") { + section("buttons") { + buttons(name: "buttons", title: "required:false, multiple:false", required: false, multiple: false, buttons: [ + [label: "foo", action: "foo"], + [label: "bar", action: "bar"] + ]) + buttons(name: "buttonsRequired", title: "required:true", required: true, multiple: false, buttons: [ + [label: "foo", action: "foo"], + [label: "bar", action: "bar"] + ]) + buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [ + [label: "foo", action: "foo"], + [label: "bar", action: "bar"] + ]) + } + section("Colored Buttons") { + buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [ + [label: "complete", action: "bar", backgroundColor: "complete"], + [label: "required", action: "bar", backgroundColor: "required"] + ]) + buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [ + [label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"], + [label: "fg: #ffac00", action: "foo", color: "#ffac00"], + [label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"] + ]) + buttons(name: "buttonsColoredString", title: "strings work too", buttons: [ + [label: "green", action: "foo", backgroundColor: "green"], + [label: "red", action: "foo", backgroundColor: "red"], + [label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"] + ]) + } + } + +} + +def imagePage() { + dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise + section("image") { + image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg" + image(name: "imageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} + +def videoPage() { + dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish this + section("video") { + // TODO: update this when there is a videoElement method + element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c26321.jpg", video: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c2631f.mp4") + } + } +} + +def flattenedPage() { + def allSections = [] + firstPage().sections.each { section -> + section.body.each { hrefElement -> + if (hrefElement.page != "flattenedPage") { + allSections += "${hrefElement.page}"().sections + } + } + } + def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {} + flattenedPage.sections = allSections + return flattenedPage +} + +def foo() { + dynamicPage(name: "deadEnd") { + + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + // TODO: subscribe to attributes, devices, locations, etc. +} diff --git a/smartapps/smartthings/feed-my-pet.src/feed-my-pet.groovy b/smartapps/smartthings/feed-my-pet.src/feed-my-pet.groovy new file mode 100644 index 00000000000..f35092bce02 --- /dev/null +++ b/smartapps/smartthings/feed-my-pet.src/feed-my-pet.groovy @@ -0,0 +1,51 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Feed My Pet + * + * Author: SmartThings + */ +definition( + name: "Feed My Pet", + namespace: "smartthings", + author: "SmartThings", + description: "Setup a schedule for when your pet is fed. Purchase any SmartThings certified pet food feeder and install the Feed My Pet app, and set the time. You and your pet are ready to go. Your life just got smarter.", + category: "Pets", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder@2x.png" +) + +preferences { + section("Choose your pet feeder...") { + input "feeder", "device.PetFeederShield", title: "Where?" + } + section("Feed my pet at...") { + input "time1", "time", title: "When?" + } +} + +def installed() +{ + schedule(time1, "scheduleCheck") +} + +def updated() +{ + unschedule() + schedule(time1, "scheduleCheck") +} + +def scheduleCheck() +{ + log.trace "scheduledFeeding" + feeder?.feed() +} diff --git a/smartapps/smartthings/flood-alert.src/flood-alert.groovy b/smartapps/smartthings/flood-alert.src/flood-alert.groovy new file mode 100644 index 00000000000..73cae0ba35a --- /dev/null +++ b/smartapps/smartthings/flood-alert.src/flood-alert.groovy @@ -0,0 +1,73 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Flood Alert + * + * Author: SmartThings + */ +definition( + name: "Flood Alert!", + namespace: "smartthings", + author: "SmartThings", + description: "Get a push notification or text message when water is detected where it doesn't belong.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/water_moisture.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/water_moisture@2x.png" +) + +preferences { + section("When there's water detected...") { + input "alarm", "capability.waterSensor", title: "Where?" + } + section("Send a notification to...") { + input("recipients", "contact", title: "Recipients", description: "Send notifications to") { + input "phone", "phone", title: "Phone number?", required: false + } + } +} + +def installed() { + subscribe(alarm, "water.wet", waterWetHandler) +} + +def updated() { + unsubscribe() + subscribe(alarm, "water.wet", waterWetHandler) +} + +def waterWetHandler(evt) { + def deltaSeconds = 60 + + def timeAgo = new Date(now() - (1000 * deltaSeconds)) + def recentEvents = alarm.eventsSince(timeAgo) + log.debug "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds" + + def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1 + + if (alreadySentSms) { + log.debug "SMS already sent to $phone within the last $deltaSeconds seconds" + } else { + def msg = "${alarm.displayName} is wet!" + log.debug "$alarm is wet, texting $phone" + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush(msg) + if (phone) { + sendSms(phone, msg) + } + } + } +} + diff --git a/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy b/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy new file mode 100644 index 00000000000..acf3ffbdf37 --- /dev/null +++ b/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy @@ -0,0 +1,247 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Foscam (connect) + * + * Author: smartthings + * Date: 2014-03-10 + */ + +definition( + name: "Foscam (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png" +) + +preferences { + page(name: "cameraDiscovery", title:"Foscam Camera Setup", content:"cameraDiscovery") + page(name: "loginToFoscam", title: "Foscam Login") +} + +//PAGES +///////////////////////////////////// +def cameraDiscovery() +{ + if(canInstallLabs()) + { + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = 3 + + def options = camerasDiscovered() ?: [] + def numFound = options.size() ?: 0 + + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //bridge discovery request every + if((refreshCount % 5) == 0) { + discoverCameras() + } + + return dynamicPage(name:"cameraDiscovery", title:"Discovery Started!", nextPage:"loginToFoscam", refreshInterval:refreshInterval, uninstall: true) { + section("Please wait while we discover your Foscam. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { + input "selectedFoscam", "enum", required:false, title:"Select Foscam (${numFound} found)", multiple:true, options:options + } + } + } + else + { + def upgradeNeeded = """To use Foscam, your Hub should be completely up to date. + + To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"cameraDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") { + paragraph "$upgradeNeeded" + } + } + + } +} + +def loginToFoscam() { + def showUninstall = username != null && password != null + return dynamicPage(name: "loginToFoscam", title: "Foscam", uninstall:showUninstall, install:true,) { + section("Log in to Foscam") { + input "username", "text", title: "Username", required: true, autoCorrect:false + input "password", "password", title: "Password", required: true, autoCorrect:false + } + } +} +//END PAGES + +///////////////////////////////////// +private discoverCameras() +{ + //add type UDP_CLIENT + def action = new physicalgraph.device.HubAction("0b4D4F5F490000000000000000000000040000000400000000000001", physicalgraph.device.Protocol.LAN, "FFFFFFFF:2710") + action.options = [type:"LAN_TYPE_UDPCLIENT"] + sendHubCommand(action) +} + +def camerasDiscovered() { + def cameras = getCameras() + def map = [:] + cameras.each { + def value = it.value.name ?: "Foscam Camera" + def key = it.value.ip + ":" + it.value.port + map["${key}"] = value + } + map +} + +///////////////////////////////////// +def getCameras() +{ + state.cameras = state.cameras ?: [:] +} + +///////////////////////////////////// +def installed() { + //log.debug "Installed with settings: ${settings}" + initialize() + + runIn(300, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 5 minutes + + //wait 5 seconds and get the deviceInfo + //log.info "calling 'getDeviceInfo()'" + //runIn(5, getDeviceInfo) +} + +///////////////////////////////////// +def updated() { + //log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +///////////////////////////////////// +def initialize() { + // remove location subscription aftwards + unsubscribe() + state.subscribe = false + + if (selectedFoscam) + { + addCameras() + } +} + +def addCameras() { + def cameras = getCameras() + + selectedFoscam.each { dni -> + def d = getChildDevice(dni) + + if(!d) + { + def newFoscam = cameras.find { (it.value.ip + ":" + it.value.port) == dni } + d = addChildDevice("smartthings", "Foscam", dni, newFoscam?.value?.hub, ["label":newFoscam?.value?.name ?: "Foscam Camera", "data":["mac": newFoscam?.value?.mac, "ip": newFoscam.value.ip, "port":newFoscam.value.port], "preferences":["username":username, "password":password]]) + + log.debug "created ${d.displayName} with id $dni" + } + else + { + log.debug "found ${d.displayName} with id $dni already exists" + } + } +} + +def getDeviceInfo() { + def devices = getAllChildDevices() + devices.each { d -> + d.getDeviceInfo() + } +} + +///////////////////////////////////// +def locationHandler(evt) { + /* + FOSCAM EXAMPLE + 4D4F5F4901000000000000000000006200000000000000 (SOF) //46 + 30303632364534443042344200 (mac) //26 + 466F7363616D5F44617274684D61756C0000000000 (name) //42 + 0A01652C (ip) //8 + FFFFFE00 (mask) //8 + 00000000 (gateway ip) //8 + 00000000 (dns) //8 + 01005800 (reserve) //8 + 01040108 (system software version) //8 + 020B0106 (app software version) //8 + 0058 (port) //4 + 01 (dhcp enabled) //2 + */ + def description = evt.description + def hub = evt?.hubId + + log.debug "GOT LOCATION EVT: $description" + + def parsedEvent = stringToMap(description) + + //FOSCAM does a UDP response with camera operate protocol:“MO_I” i.e. "4D4F5F49" + if (parsedEvent?.type == "LAN_TYPE_UDPCLIENT" && parsedEvent?.payload?.startsWith("4D4F5F49")) + { + def unpacked = [:] + unpacked.mac = parsedEvent.mac.toString() + unpacked.name = hexToString(parsedEvent.payload[72..113]).trim() + unpacked.ip = parsedEvent.payload[114..121] + unpacked.subnet = parsedEvent.payload[122..129] + unpacked.gateway = parsedEvent.payload[130..137] + unpacked.dns = parsedEvent.payload[138..145] + unpacked.reserve = parsedEvent.payload[146..153] + unpacked.sysVersion = parsedEvent.payload[154..161] + unpacked.appVersion = parsedEvent.payload[162..169] + unpacked.port = parsedEvent.payload[170..173] + unpacked.dhcp = parsedEvent.payload[174..175] + unpacked.hub = hub + + def cameras = getCameras() + if (!(cameras."${parsedEvent.mac.toString()}")) + { + cameras << [("${parsedEvent.mac.toString()}"):unpacked] + } + } +} + +///////////////////////////////////// +private Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} + +private String hexToString(String txtInHex) +{ + byte [] txtInByte = new byte [txtInHex.length() / 2]; + int j = 0; + for (int i = 0; i < txtInHex.length(); i += 2) + { + txtInByte[j++] = Byte.parseByte(txtInHex.substring(i, i + 2), 16); + } + return new String(txtInByte); +} diff --git a/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy b/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy new file mode 100644 index 00000000000..4cabc5c3507 --- /dev/null +++ b/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy @@ -0,0 +1,126 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Garage Door Monitor + * + * Author: SmartThings + */ +definition( + name: "Garage Door Monitor", + namespace: "smartthings", + author: "SmartThings", + description: "Monitor your garage door and get a text message if it is open too long", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact@2x.png" +) + +preferences { + section("When the garage door is open...") { + input "multisensor", "capability.threeAxis", title: "Which?" + } + section("For too long...") { + input "maxOpenTime", "number", title: "Minutes?" + } + section("Text me at (optional, sends a push notification if not specified)...") { + input("recipients", "contact", title: "Notify", description: "Send notifications to") { + input "phone", "phone", title: "Phone number?", required: false + } + } +} + +def installed() +{ + subscribe(multisensor, "acceleration", accelerationHandler) +} + +def updated() +{ + unsubscribe() + subscribe(multisensor, "acceleration", accelerationHandler) +} + +def accelerationHandler(evt) { + def latestThreeAxisState = multisensor.threeAxisState // e.g.: 0,0,-1000 + if (latestThreeAxisState) { + def isOpen = Math.abs(latestThreeAxisState.xyzValue.z) > 250 // TODO: Test that this value works in most cases... + def isNotScheduled = state.status != "scheduled" + + if (!isOpen) { + clearSmsHistory() + clearStatus() + } + + if (isOpen && isNotScheduled) { + runIn(maxOpenTime * 60, takeAction, [overwrite: false]) + state.status = "scheduled" + } + + } + else { + log.warn "COULD NOT FIND LATEST 3-AXIS STATE FOR: ${multisensor}" + } +} + +def takeAction(){ + if (state.status == "scheduled") + { + def deltaMillis = 1000 * 60 * maxOpenTime + def timeAgo = new Date(now() - deltaMillis) + def openTooLong = multisensor.threeAxisState.dateCreated.toSystemDate() < timeAgo + + def recentTexts = state.smsHistory.find { it.sentDate.toSystemDate() > timeAgo } + + if (!recentTexts) { + sendTextMessage() + } + runIn(maxOpenTime * 60, takeAction, [overwrite: false]) + } else { + log.trace "Status is no longer scheduled. Not sending text." + } +} + +def sendTextMessage() { + log.debug "$multisensor was open too long, texting $phone" + + updateSmsHistory() + def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1) + def msg = "Your ${multisensor.label ?: multisensor.name} has been open for more than ${openMinutes} minutes!" + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (phone) { + sendSms(phone, msg) + } else { + sendPush msg + } + } +} + +def updateSmsHistory() { + if (!state.smsHistory) state.smsHistory = [] + + if(state.smsHistory.size() > 9) { + log.debug "SmsHistory is too big, reducing size" + state.smsHistory = state.smsHistory[-9..-1] + } + state.smsHistory << [sentDate: new Date().toSystemFormat()] +} + +def clearSmsHistory() { + state.smsHistory = null +} + +def clearStatus() { + state.status = null +} diff --git a/smartapps/smartthings/garage-door-opener.src/garage-door-opener.groovy b/smartapps/smartthings/garage-door-opener.src/garage-door-opener.groovy new file mode 100644 index 00000000000..30ffdf09cf7 --- /dev/null +++ b/smartapps/smartthings/garage-door-opener.src/garage-door-opener.groovy @@ -0,0 +1,52 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Garage Door Opener + * + * Author: SmartThings + */ +definition( + name: "Garage Door Opener", + namespace: "smartthings", + author: "SmartThings", + description: "Open your garage door when a switch is turned on.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_outlet@2x.png" +) + +preferences { + section("When the garage door switch is turned on, open the garage door...") { + input "switch1", "capability.switch" + } +} + +def installed() { + subscribe(app, appTouchHandler) + subscribeToCommand(switch1, "on", onCommand) +} + +def updated() { + unsubscribe() + subscribe(app, appTouchHandler) + subscribeToCommand(switch1, "on", onCommand) +} + +def appTouch(evt) { + log.debug "appTouch: $evt.value, $evt" + switch1?.on() +} + +def onCommand(evt) { + log.debug "onCommand: $evt.value, $evt" + switch1?.off(delay: 3000) +} diff --git a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy new file mode 100644 index 00000000000..8b950fcff83 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy @@ -0,0 +1,833 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Gentle Wake Up + * + * Author: Steve Vlaminck + * Date: 2013-03-11 + * + * https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png + * https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime%402x.png + * Gentle Wake Up turns on your lights slowly, allowing you to wake up more + * naturally. Once your lights have reached full brightness, optionally turn on + * more things, or send yourself a text for a more gentle nudge into the waking + * world (you may want to set your normal alarm as a backup plan). + * + */ +definition( + name: "Gentle Wake Up", + namespace: "smartthings", + author: "SmartThings", + description: "Gentle Wake Up dims your lights slowly, allowing you to wake up more naturally. Once your lights have finished dimming, optionally turn on more things or send yourself a text for a more gentle nudge into the waking world (you may want to set your normal alarm as a backup plan).", + category: "Health & Wellness", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png" +) + +preferences { + page(name: "rootPage") + page(name: "schedulingPage") + page(name: "completionPage") + page(name: "numbersPage") +} + +def rootPage() { + dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) { + + section { + input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true) + } + + if (dimmers) { + + section { + href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete") + } + + section { + href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "") + } + + section { + href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription()) + } + + section { + // TODO: fancy label + label(title: "Label this SmartApp", required: false, defaultValue: "") + } + } + } +} + +def numbersPage() { + dynamicPage(name:"numbersPage", title:"") { + + section { + paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers)) + } + + section { + input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30) + } + + section { + input(name: "startLevel", type: "number", range: "0..99", title: "From this level", defaultValue: defaultStart(), description: "Current Level", required: false, multiple: false) + input(name: "endLevel", type: "number", range: "0..99", title: "To this level", defaultValue: defaultEnd(), description: "Between 0 and 99", required: true, multiple: false) + } + + def colorDimmers = dimmersWithSetColorCommand() + if (colorDimmers) { + section { + input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true") + } + } + } +} + +def defaultStart() { + if (usesOldSettings() && direction && direction == "Down") { + return 99 + } + return 0 +} + +def defaultEnd() { + if (usesOldSettings() && direction && direction == "Down") { + return 0 + } + return 99 +} + +def startLevelLabel() { + if (usesOldSettings()) { // using old settings + if (direction && direction == "Down") { // 99 -> 1 + return "99%" + } + return "0%" + } + return hasStartLevel() ? "${startLevel}%" : "Current Level" +} + +def endLevelLabel() { + if (usesOldSettings()) { + if (direction && direction == "Down") { // 99 -> 1 + return "0%" + } + return "99%" + } + return "${endLevel}%" +} + +def schedulingPage() { + dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") { + + section { + input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]) + } + + section { + input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true) + if (modeStart) { + input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false) + } + } + + section { + input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false) + } + + } +} + +def completionPage() { + dynamicPage(name: "completionPage", title: "Completion Rules") { + + section("Switches") { + input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true) + if (completionSwitches || androidClient()) { + input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on") + input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)") + } + } + + section("Notifications") { + input("recipients", "contact", title: "Send notifications to") { + input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false) + input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false) + } + input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false) + input(name: "completionMessage", type: "text", title: "With This Message", description: null, required: false) + } + + section("Modes and Phrases") { + input(name: "completionMode", type: "mode", title: "Change ${location.name} Mode To", description: null, required: false) + input(name: "completionPhrase", type: "enum", title: "Execute The Phrase", description: null, required: false, multiple: false, options: location.helloHome.getPhrases().label) + } + + section("Delay") { + input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false) + } + } +} + +// ======================================================== +// Handlers +// ======================================================== + +def installed() { + log.debug "Installing 'Gentle Wake Up' with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updating 'Gentle Wake Up' with settings: ${settings}" + unschedule() + + initialize() +} + +private initialize() { + stop() + + if (startTime) { + log.debug "scheduling dimming routine to run at $startTime" + schedule(startTime, "scheduledStart") + } + + // TODO: make this an option + subscribe(app, appHandler) + + subscribe(location, locationHandler) +} + +def appHandler(evt) { + log.debug "appHandler evt: ${evt.value}" + if (evt.value == "touch") { + if (atomicState.running) { + stop() + } else { + start() + } + } +} + +def locationHandler(evt) { + log.debug "locationHandler evt: ${evt.value}" + + if (!modeStart) { + return + } + + def isSpecifiedMode = (evt.value == modeStart) + def modeStopIsTrue = (modeStop && modeStop != "false") + + if (isSpecifiedMode && canStartAutomatically()) { + start() + } else if (!isSpecifiedMode && modeStopIsTrue) { + stop() + } + +} + +// ======================================================== +// Scheduling +// ======================================================== + +def scheduledStart() { + if (canStartAutomatically()) { + start() + } +} + +def start() { + log.trace "START" + + setLevelsInState() + + atomicState.running = true + + atomicState.start = new Date().getTime() + + schedule("0 * * * * ?", "healthCheck") + increment() +} + +def stop() { + log.trace "STOP" + + atomicState.running = false + atomicState.start = 0 + + unschedule("healthCheck") +} + +private healthCheck() { + log.trace "'Gentle Wake Up' healthCheck" + + if (!atomicState.running) { + return + } + + increment() +} + +// ======================================================== +// Setting levels +// ======================================================== + + +private increment() { + + if (!atomicState.running) { + return + } + + def percentComplete = completionPercentage() + + if (percentComplete > 99) { + percentComplete = 99 + } + + updateDimmers(percentComplete) + + if (percentComplete < 99) { + + def runAgain = stepDuration() + log.debug "Rescheduling to run again in ${runAgain} seconds" + + runIn(runAgain, 'increment', [overwrite: true]) + + } else { + + int completionDelay = completionDelaySeconds() + if (completionDelay) { + log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now" + runIn(completionDelay, 'completion', [overwrite: true]) + unschedule("healthCheck") + // don't let the health check start incrementing again while we wait for the delayed execution of completion + } else { + log.debug "Finished with steps. Execution completion" + completion() + } + + } +} + + +def updateDimmers(percentComplete) { + dimmers.each { dimmer -> + + def nextLevel = dynamicLevel(dimmer, percentComplete) + + if (nextLevel == 0) { + + dimmer.off() + + } else { + + def shouldChangeColors = (colorize && colorize != "false") + def canChangeColors = hasSetColorCommand(dimmer) + + log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}" + + if (shouldChangeColors && canChangeColors) { + dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel]) + } else { + dimmer.setLevel(nextLevel) + } + + } + } +} + +int dynamicLevel(dimmer, percentComplete) { + def start = atomicState.startLevels[dimmer.id] + def end = dynamicEndLevel() + + if (!percentComplete) { + return start + } + + def totalDiff = end - start + def actualPercentage = percentComplete / 100 + def percentOfTotalDiff = totalDiff * actualPercentage + + (start + percentOfTotalDiff) as int +} + +// ======================================================== +// Completion +// ======================================================== + +private completion() { + log.trace "Starting completion block" + + if (!atomicState.running) { + return + } + + stop() + + handleCompletionSwitches() + + handleCompletionMessaging() + + handleCompletionModesAndPhrases() + +} + +private handleCompletionSwitches() { + completionSwitches.each { completionSwitch -> + + def isDimmer = hasSetLevelCommand(completionSwitch) + + if (completionSwitchesLevel && isDimmer) { + completionSwitch.setLevel(completionSwitchesLevel) + } else { + def command = completionSwitchesState ?: "on" + completionSwitch."${command}"() + } + } +} + +private handleCompletionMessaging() { + if (completionMessage) { + if (location.contactBookEnabled) { + sendNotificationToContacts(completionMessage, recipients) + } else { + if (completionPhoneNumber) { + sendSms(completionPhoneNumber, completionMessage) + } + if (completionPush) { + sendPush(completionMessage) + } + } + if (completionMusicPlayer) { + speak(completionMessage) + } + } +} + +private handleCompletionModesAndPhrases() { + + if (completionMode) { + setLocationMode(completionMode) + } + + if (completionPhrase) { + location.helloHome.execute(completionPhrase) + } + +} + +def speak(message) { + def sound = textToSpeech(message) + def soundDuration = (sound.duration as Integer) + 2 + log.debug "Playing $sound.uri" + completionMusicPlayer.playTrack(sound.uri) + log.debug "Scheduled resume in $soundDuration sec" + runIn(soundDuration, resumePlaying, [overwrite: true]) +} + +def resumePlaying() { + log.trace "resumePlaying()" + def sonos = completionMusicPlayer + if (sonos) { + def currentTrack = sonos.currentState("trackData").jsonValue + if (currentTrack.status == "playing") { + sonos.playTrack(currentTrack) + } else { + sonos.setTrack(currentTrack) + } + } +} + +// ======================================================== +// Helpers +// ======================================================== + +def setLevelsInState() { + def startLevels = [:] + dimmers.each { dimmer -> + if (usesOldSettings()) { + startLevels[dimmer.id] = defaultStart() + } else if (hasStartLevel()) { + startLevels[dimmer.id] = startLevel + } else { + def dimmerIsOff = dimmer.currentValue("switch") == "off" + startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level") + } + } + + atomicState.startLevels = startLevels +} + +def canStartAutomatically() { + + def today = new Date().format("EEEE") + log.debug "today: ${today}, days: ${days}" + + if (!days || days.contains(today)) {// if no days, assume every day + return true + } + + log.trace "should not run" + return false +} + +def completionPercentage() { + log.trace "checkingTime" + + if (!atomicState.running) { + return + } + + int now = new Date().getTime() + int diff = now - atomicState.start + int totalRunTime = totalRunTimeMillis() + int percentOfRunTime = (diff / totalRunTime) * 100 + log.debug "percentOfRunTime: ${percentOfRunTime}" + + percentOfRunTime +} + +int totalRunTimeMillis() { + int minutes = sanitizeInt(duration, 30) + def seconds = minutes * 60 + def millis = seconds * 1000 + return millis as int +} + +int dynamicEndLevel() { + if (usesOldSettings()) { + if (direction && direction == "Down") { + return 0 + } + return 99 + } + return endLevel as int +} + +def getHue(dimmer, level) { + def start = atomicState.startLevels[dimmer.id] as int + def end = dynamicEndLevel() + if (start > end) { + return getDownHue(level) + } else { + return getUpHue(level) + } +} + +def getUpHue(level) { + getBlueHue(level) +} + +def getDownHue(level) { + getRedHue(level) +} + +private getBlueHue(level) { + if (level < 5) return 72 + if (level < 10) return 71 + if (level < 15) return 70 + if (level < 20) return 69 + if (level < 25) return 68 + if (level < 30) return 67 + if (level < 35) return 66 + if (level < 40) return 65 + if (level < 45) return 64 + if (level < 50) return 63 + if (level < 55) return 62 + if (level < 60) return 61 + if (level < 65) return 60 + if (level < 70) return 59 + if (level < 75) return 58 + if (level < 80) return 57 + if (level < 85) return 56 + if (level < 90) return 55 + if (level < 95) return 54 + if (level >= 95) return 53 +} + +private getRedHue(level) { + if (level < 6) return 1 + if (level < 12) return 2 + if (level < 18) return 3 + if (level < 24) return 4 + if (level < 30) return 5 + if (level < 36) return 6 + if (level < 42) return 7 + if (level < 48) return 8 + if (level < 54) return 9 + if (level < 60) return 10 + if (level < 66) return 11 + if (level < 72) return 12 + if (level < 78) return 13 + if (level < 84) return 14 + if (level < 90) return 15 + if (level < 96) return 16 + if (level >= 96) return 17 +} + +private hasSetLevelCommand(device) { + def isDimmer = false + device.supportedCommands.each { + if (it.name.contains("setLevel")) { + isDimmer = true + } + } + return isDimmer +} + +private hasSetColorCommand(device) { + def hasColor = false + device.supportedCommands.each { + if (it.name.contains("setColor")) { + hasColor = true + } + } + return hasColor +} + +private dimmersWithSetColorCommand() { + def colorDimmers = [] + dimmers.each { dimmer -> + if (hasSetColorCommand(dimmer)) { + colorDimmers << dimmer + } + } + return colorDimmers +} + +private int sanitizeInt(i, int defaultValue = 0) { + try { + if (!i) { + return defaultValue + } else { + return i as int + } + } + catch (Exception e) { + log.debug e + return defaultValue + } +} + +private completionDelaySeconds() { + int completionDelayMinutes = sanitizeInt(completionDelay) + int completionDelaySeconds = (completionDelayMinutes * 60) + return completionDelaySeconds ?: 0 +} + +private stepDuration() { + int minutes = sanitizeInt(duration, 30) + int stepDuration = (minutes * 60) / 100 + return stepDuration ?: 1 +} + +private debug(message) { + log.debug "${message}\nstate: ${state}" +} + +public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" } + +public humanReadableStartDate() { + new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime)) +} + +def fancyString(listOfStrings) { + + def fancify = { list -> + return list.collect { + def label = it + if (list.size() > 1 && it == list[-1]) { + label = "and ${label}" + } + label + }.join(", ") + } + + return fancify(listOfStrings) +} + +def fancyDeviceString(devices = []) { + fancyString(devices.collect { deviceLabel(it) }) +} + +def deviceLabel(device) { + return device.label ?: device.name +} + +def schedulingHrefDescription() { + + def descriptionParts = [] + if (days) { + descriptionParts << "On ${fancyString(days)}," + } + + descriptionParts << "${fancyDeviceString(dimmers)} will start dimming" + + if (startTime) { + descriptionParts << "at ${humanReadableStartDate()}" + } + + if (modeStart) { + if (startTime) { + descriptionParts << "or" + } + descriptionParts << "when ${location.name} enters '${modeStart}' mode" + } + + if (descriptionParts.size() <= 1) { + // dimmers will be in the list no matter what. No rules are set if only dimmers are in the list + return null + } + + return descriptionParts.join(" ") +} + +def completionHrefDescription() { + + def descriptionParts = [] + def example = "Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed" + + if (completionSwitches) { + def switchesList = [] + def dimmersList = [] + + + completionSwitches.each { + def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false + + if (isDimmer) { + dimmersList << deviceLabel(it) + } + + if (!isDimmer) { + switchesList << deviceLabel(it) + } + } + + + if (switchesList) { + descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}." + } + + if (dimmersList) { + descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%." + } + + } + + if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) { + def messageParts = [] + + if (completionMusicPlayer) { + messageParts << "spoken" + } + if (completionPhoneNumber) { + messageParts << "sent as a text" + } + if (completionPush) { + messageParts << "sent as a push notification" + } + + descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}." + } + + if (completionMode) { + descriptionParts << "The mode will be changed to '${completionMode}'." + } + + if (completionPhrase) { + descriptionParts << "The phrase '${completionPhrase}' will be executed." + } + + return descriptionParts.join(" ") +} + +def numbersPageHrefDescription() { + def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}" + if (colorize) { + def colorDimmers = dimmersWithSetColorCommand() + if (colorDimmers == dimmers) { + title += " and will gradually change color." + } else { + title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color." + } + } + return title +} + +def hueSatToHex(h, s) { + def convertedRGB = hslToRgb(h, s, 0.5) + return rgbToHex(convertedRGB) +} + +def hslToRgb(h, s, l) { + def r, g, b; + + if (s == 0) { + r = g = b = l; // achromatic + } else { + def hue2rgb = { p, q, t -> + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + + def q = l < 0.5 ? l * (1 + s) : l + s - l * s; + def p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [r * 255, g * 255, b * 255]; +} + +def rgbToHex(red, green, blue) { + def toHex = { + int n = it as int; + n = Math.max(0, Math.min(n, 255)); + def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] + + def firstDecimal = ((n - n % 16) / 16) as int + def secondDecimal = (n % 16) as int + + return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}" + } + + def rgbToHex = { r, g, b -> + return toHex(r) + toHex(g) + toHex(b) + } + + return rgbToHex(red, green, blue) +} + +def usesOldSettings() { + !hasEndLevel() +} + +def hasStartLevel() { + return (startLevel != null && startLevel != "") +} + +def hasEndLevel() { + return (endLevel != null && endLevel != "") +} diff --git a/smartapps/smartthings/good-night.src/good-night.groovy b/smartapps/smartthings/good-night.src/good-night.groovy new file mode 100644 index 00000000000..3561eabd339 --- /dev/null +++ b/smartapps/smartthings/good-night.src/good-night.groovy @@ -0,0 +1,198 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Good Night + * + * Author: SmartThings + * Date: 2013-03-07 + */ +definition( + name: "Good Night", + namespace: "smartthings", + author: "SmartThings", + description: "Changes mode when motion ceases after a specific time of night.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/good-night.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/good-night@2x.png" +) + +preferences { + section("When there is no motion on any of these sensors") { + input "motionSensors", "capability.motionSensor", title: "Where?", multiple: true + } + section("For this amount of time") { + input "minutes", "number", title: "Minutes?" + } + section("After this time of day") { + input "timeOfDay", "time", title: "Time?" + } + section("And (optionally) these switches are all off") { + input "switches", "capability.switch", multiple: true, required: false + } + section("Change to this mode") { + input "newMode", "mode", title: "Mode?" + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phoneNumber", "phone", title: "Send a Text Message?", required: false + } + } + +} + +def installed() { + log.debug "Current mode = ${location.mode}" + createSubscriptions() +} + +def updated() { + log.debug "Current mode = ${location.mode}" + unsubscribe() + createSubscriptions() +} + +def createSubscriptions() +{ + subscribe(motionSensors, "motion.active", motionActiveHandler) + subscribe(motionSensors, "motion.inactive", motionInactiveHandler) + subscribe(switches, "switch.off", switchOffHandler) + subscribe(location, modeChangeHandler) + + if (state.modeStartTime == null) { + state.modeStartTime = 0 + } +} + +def modeChangeHandler(evt) { + state.modeStartTime = now() +} + +def switchOffHandler(evt) { + if (correctMode() && correctTime()) { + if (allQuiet() && switchesOk()) { + takeActions() + } + } +} + +def motionActiveHandler(evt) +{ + log.debug "Motion active" +} + +def motionInactiveHandler(evt) +{ + // for backward compatibility + if (state.modeStartTime == null) { + subscribe(location, modeChangeHandler) + state.modeStartTime = 0 + } + + if (correctMode() && correctTime()) { + runIn(minutes * 60, scheduleCheck, [overwrite: false]) + } +} + +def scheduleCheck() +{ + log.debug "scheduleCheck, currentMode = ${location.mode}, newMode = $newMode" + + if (correctMode() && correctTime()) { + if (allQuiet() && switchesOk()) { + takeActions() + } + } +} + +private takeActions() { + def message = "Goodnight! SmartThings changed the mode to '$newMode'" + send(message) + setLocationMode(newMode) + log.debug message +} + +private correctMode() { + if (location.mode != newMode) { + true + } else { + log.debug "Location is already in the desired mode: doing nothing" + false + } +} + +private correctTime() { + def t0 = now() + def modeStartTime = new Date(state.modeStartTime) + def startTime = timeTodayAfter(modeStartTime, timeOfDay, location.timeZone) + if (t0 >= startTime.time) { + true + } else { + log.debug "The current time of day (${new Date(t0)}), is not in the correct time window ($startTime): doing nothing" + false + } +} + +private switchesOk() { + def result = true + for (it in (switches ?: [])) { + if (it.currentSwitch == "on") { + result = false + break + } + } + log.debug "Switches are all off: $result" + result +} + +private allQuiet() { + def threshold = 1000 * 60 * minutes - 1000 + def states = motionSensors.collect { it.currentState("motion") ?: [:] }.sort { a, b -> b.dateCreated <=> a.dateCreated } + if (states) { + if (states.find { it.value == "active" }) { + log.debug "Found active state" + false + } else { + def sensor = states.first() + def elapsed = now() - sensor.rawDateCreated.time + if (elapsed >= threshold) { + log.debug "No active states, and enough time has passed" + true + } else { + log.debug "No active states, but not enough time has passed" + false + } + } + } else { + log.debug "No states to check for activity" + true + } +} + +private send(msg) { + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phoneNumber) { + log.debug("sending text message") + sendSms(phoneNumber, msg) + } + } + + log.debug msg +} diff --git a/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy b/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy new file mode 100644 index 00000000000..2797c09e8bc --- /dev/null +++ b/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy @@ -0,0 +1,111 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Greetings Earthling + * + * Author: SmartThings + * Date: 2013-03-07 + */ +definition( + name: "Greetings Earthling", + namespace: "smartthings", + author: "SmartThings", + description: "Monitors a set of presence detectors and triggers a mode change when someone arrives at home.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png" +) + +preferences { + + section("When one of these people arrive at home") { + input "people", "capability.presenceSensor", multiple: true + } + section("Change to this mode") { + input "newMode", "mode", title: "Mode?" + } + section("False alarm threshold (defaults to 10 min)") { + input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phone", "phone", title: "Send a Text Message?", required: false + } + } + +} + +def installed() { + log.debug "Installed with settings: ${settings}" + log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + subscribe(people, "presence", presence) +} + +def updated() { + log.debug "Updated with settings: ${settings}" + log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + unsubscribe() + subscribe(people, "presence", presence) +} + +def presence(evt) +{ + log.debug "evt.name: $evt.value" + def threshold = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? (falseAlarmThreshold * 60 * 1000) as Long : 10 * 60 * 1000L + + if (location.mode != newMode) { + + def t0 = new Date(now() - threshold) + if (evt.value == "present") { + + def person = getPerson(evt) + def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"} + if (recentNotPresent) { + log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago" + } + else { + def message = "${person.displayName} arrived at home, changing mode to '${newMode}'" + log.info message + send(message) + setLocationMode(newMode) + } + } + } + else { + log.debug "mode is the same, not evaluating" + } +} + +private getPerson(evt) +{ + people.find{evt.deviceId == it.id} +} + +private send(msg) { + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phone) { + log.debug("sending text message") + sendSms(phone, msg) + } + } + + log.debug msg +} diff --git a/smartapps/smartthings/habit-helper.src/habit-helper.groovy b/smartapps/smartthings/habit-helper.src/habit-helper.groovy new file mode 100644 index 00000000000..6e8194ce0cd --- /dev/null +++ b/smartapps/smartthings/habit-helper.src/habit-helper.groovy @@ -0,0 +1,68 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Habit Helper + * Every day at a specific time, get a text reminding you about your habit + * + * Author: SmartThings + */ +definition( + name: "Habit Helper", + namespace: "smartthings", + author: "SmartThings", + description: "Add something you want to be reminded about each day and get a text message to help you form positive habits.", + category: "Family", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png" +) + +preferences { + section("Remind me about..."){ + input "message1", "text", title: "What?" + } + section("At what time?"){ + input "time1", "time", title: "When?" + } + section("Text me at..."){ + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + +def installed() +{ + schedule(time1, "scheduleCheck") +} + +def updated() +{ + unschedule() + schedule(time1, "scheduleCheck") +} + +def scheduleCheck() +{ + log.trace "scheduledCheck" + + def message = message1 ?: "SmartThings - Habit Helper Reminder!" + + if (location.contactBookEnabled) { + log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}" + sendNotificationToContacts(message, recipients) + } + else { + + log.debug "Texting reminder: ($message) to $phone1" + sendSms(phone1, message) + } +} diff --git a/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy b/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy new file mode 100644 index 00000000000..31faa2d2d19 --- /dev/null +++ b/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy @@ -0,0 +1,75 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Has Barkley Been Fed + * + * Author: SmartThings + */ +definition( + name: "Has Barkley Been Fed?", + namespace: "smartthings", + author: "SmartThings", + description: "Setup a schedule to be reminded to feed your pet. Purchase any SmartThings certified pet food feeder and install the Feed My Pet app, and set the time. You and your pet are ready to go. Your life just got smarter.", + category: "Pets", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder@2x.png" +) + +preferences { + section("Choose your pet feeder...") { + input "feeder1", "capability.contactSensor", title: "Where?" + } + section("Feed my pet at...") { + input "time1", "time", title: "When?" + } + section("Text me if I forget...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + +def installed() +{ + schedule(time1, "scheduleCheck") +} + +def updated() +{ + unsubscribe() //TODO no longer subscribe like we used to - clean this up after all apps updated + unschedule() + schedule(time1, "scheduleCheck") +} + +def scheduleCheck() +{ + log.trace "scheduledCheck" + + def midnight = (new Date()).clearTime() + def now = new Date() + def feederEvents = feeder1.eventsBetween(midnight, now) + log.trace "Found ${feederEvents?.size() ?: 0} feeder events since $midnight" + def feederOpened = feederEvents.count { it.value && it.value == "open" } > 0 + + if (feederOpened) { + log.debug "Feeder was opened since $midnight, no SMS required" + } else { + if (location.contactBookEnabled) { + log.debug "Feeder was not opened since $midnight, texting contacts:${recipients?.size()}" + sendNotificationToContacts("No one has fed the dog", recipients) + } + else { + log.debug "Feeder was not opened since $midnight, texting $phone1" + sendSms(phone1, "No one has fed the dog") + } + } +} diff --git a/smartapps/smartthings/hub-ip-notifier.src/hub-ip-notifier.groovy b/smartapps/smartthings/hub-ip-notifier.src/hub-ip-notifier.groovy new file mode 100644 index 00000000000..46fe147e326 --- /dev/null +++ b/smartapps/smartthings/hub-ip-notifier.src/hub-ip-notifier.groovy @@ -0,0 +1,80 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Hub IP Notifier + * + * Author: luke + * Date: 2014-01-28 + */ +definition( + name: "Hub IP Notifier", + namespace: "smartthings", + author: "SmartThings", + description: "Listen for local IP changes when your hub registers.", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png" +) + +preferences { + page(name: "pageWithIp", title: "Hub IP Notifier", install: true) + +} + +def pageWithIp() { + def currentIp = state.localip ?: 'unknown' + def registerDate = state.lastRegister ?: null + dynamicPage(name: "pageWithIp", title: "Hub IP Notifier", install: true, uninstall: true) { + section("When Hub Comes Online") { + input "hub", "hub", title: "Select a hub" + } + section("Last Registration Details") { + if(hub && registerDate) { + paragraph """Your hub last registered with IP: +$currentIp +on: +$registerDate""" + } else if (hub && !registerDate) { + paragraph "Your hub has not (re)registered since you installed this app" + } else { + paragraph "Check back here after installing to see the current IP of your hub" + } + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(hub, "hubInfo", registrationHandler, [filterEvents: false]) +} + +def registrationHandler(evt) { + def hubInfo = evt.description.split(',').inject([:]) { map, token -> + token.split(':').with { map[it[0].trim()] = it[1] } + map + } + state.localip = hubInfo.localip + state.lastRegister = new Date() + sendNotificationEvent("${hub.name} registered in prod with IP: ${hubInfo.localip}") +} diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy new file mode 100644 index 00000000000..5726108d1df --- /dev/null +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -0,0 +1,723 @@ +/** + * Hue Service Manager + * + * Author: Juan Risso (juan@smartthings.com) + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Hue (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png" +) + +preferences { + page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5) + page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5) + page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5) + page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5) +} + +def mainPage() { + if(canInstallLabs()) { + def bridges = bridgesDiscovered() + if (state.username && bridges) { + return bulbDiscovery() + } else { + return bridgeDiscovery() + } + } else { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") { + paragraph "$upgradeNeeded" + } + } + } +} + +def bridgeDiscovery(params=[:]) +{ + def bridges = bridgesDiscovered() + int bridgeRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int + state.bridgeRefreshCount = bridgeRefreshCount + 1 + def refreshInterval = 3 + + def options = bridges ?: [] + def numFound = options.size() ?: 0 + + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //bridge discovery request every 15 //25 seconds + if((bridgeRefreshCount % 5) == 0) { + discoverBridges() + } + + //setup.xml request every 3 seconds except on discoveries + if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) { + verifyHueBridges() + } + + return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) { + section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { + input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options + } + } +} + +def bridgeLinking() +{ + int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int + state.linkRefreshcount = linkRefreshcount + 1 + def refreshInterval = 3 + + def nextPage = "" + def title = "Linking with your Hue" + def paragraphText = "Press the button on your Hue Bridge to setup a link." + if (state.username) { //if discovery worked + nextPage = "bulbDiscovery" + title = "Success! - click 'Next'" + paragraphText = "Linking to your hub was a success! Please click 'Next'!" + } + + if((linkRefreshcount % 2) == 0 && !state.username) { + sendDeveloperReq() + } + + return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) { + section("Button Press") { + paragraph """${paragraphText}""" + } + } +} + +def bulbDiscovery() +{ + int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int + state.bulbRefreshCount = bulbRefreshCount + 1 + def refreshInterval = 3 + + def options = bulbsDiscovered() ?: [] + def numFound = options.size() ?: 0 + + if((bulbRefreshCount % 3) == 0) { + discoverHueBulbs() + } + + return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { + section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { + input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options + } + section { + def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges" + href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true] + + } + } +} + +private discoverBridges() { + sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN)) +} + +private sendDeveloperReq() { + def token = app.id + def host = getBridgeIP() + sendHubCommand(new physicalgraph.device.HubAction([ + method: "POST", + path: "/api", + headers: [ + HOST: host + ], + body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}")) +} + +private discoverHueBulbs() { + def host = getBridgeIP() + sendHubCommand(new physicalgraph.device.HubAction([ + method: "GET", + path: "/api/${state.username}/lights", + headers: [ + HOST: host + ]], "${selectedHue}")) +} + +private verifyHueBridge(String deviceNetworkId, String host) { + sendHubCommand(new physicalgraph.device.HubAction([ + method: "GET", + path: "/description.xml", + headers: [ + HOST: host + ]], deviceNetworkId)) +} + +private verifyHueBridges() { + def devices = getHueBridges().findAll { it?.value?.verified != true } + devices.each { + def ip = convertHexToIP(it.value.networkAddress) + def port = convertHexToInt(it.value.deviceAddress) + verifyHueBridge("${it.value.mac}", (ip + ":" + port)) + } +} + +Map bridgesDiscovered() { + def vbridges = getVerifiedHueBridges() + def map = [:] + vbridges.each { + def value = "${it.value.name}" + def key = "${it.value.mac}" + map["${key}"] = value + } + map +} + +Map bulbsDiscovered() { + def bulbs = getHueBulbs() + def map = [:] + if (bulbs instanceof java.util.Map) { + bulbs.each { + def value = "${it?.value?.name}" + def key = app.id +"/"+ it?.value?.id + map["${key}"] = value + } + } else { //backwards compatable + bulbs.each { + def value = "${it?.name}" + def key = app.id +"/"+ it?.id + map["${key}"] = value + } + } + map +} + +def getHueBulbs() { + state.bulbs = state.bulbs ?: [:] +} + +def getHueBridges() { + state.bridges = state.bridges ?: [:] +} + +def getVerifiedHueBridges() { + getHueBridges().findAll{ it?.value?.verified == true } +} + +def installed() { + log.trace "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.trace "Updated with settings: ${settings}" + unschedule() + unsubscribe() + initialize() +} + +def initialize() { + log.debug "Initializing" + state.subscribe = false + state.bridgeSelectedOverride = false + def bridge = null + + if (selectedHue) { + addBridge() + bridge = getChildDevice(selectedHue) + subscribe(bridge, "bulbList", bulbListHandler) + } + + if (selectedBulbs) { + addBulbs() + doDeviceSync() + runEvery5Minutes("doDeviceSync") + } +} + +def manualRefresh() { + unschedule() + unsubscribe() + doDeviceSync() + runEvery5Minutes("doDeviceSync") +} + +def uninstalled(){ + state.bridges = [:] + state.subscribe = false +} + +// Handles events to add new bulbs +def bulbListHandler(evt) { + def bulbs = [:] + log.trace "Adding bulbs to state..." + state.bridgeProcessedLightList = true + evt.jsonData.each { k,v -> + log.trace "$k: $v" + if (v instanceof Map) { + bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value] + } + } + state.bulbs = bulbs + log.info "${bulbs.size()} bulbs found" +} + +def addBulbs() { + def bulbs = getHueBulbs() + selectedBulbs.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newHueBulb + if (bulbs instanceof java.util.Map) { + newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } + if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) { + d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) + } else { + d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) + } + } else { + //backwards compatable + newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } + d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) + } + + log.debug "created ${d.displayName} with id $dni" + d.refresh() + } else { + log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'" + if (bulbs instanceof java.util.Map) { + def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } + if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") { + d.setDeviceType("Hue Lux Bulb") + } + } + } + } +} + +def addBridge() { + def vbridges = getVerifiedHueBridges() + def vbridge = vbridges.find {"${it.value.mac}" == selectedHue} + + if(vbridge) { + def d = getChildDevice(selectedHue) + if(!d) { + // compatibility with old devices + def newbridge = true + childDevices.each { + if (it.getDeviceDataByName("mac")) { + def newDNI = "${it.getDeviceDataByName("mac")}" + if (newDNI != it.deviceNetworkId) { + def oldDNI = it.deviceNetworkId + log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" + it.setDeviceNetworkId("${newDNI}") + if (oldDNI == selectedHue) + app.updateSetting("selectedHue", newDNI) + newbridge = false + } + } + } + if (newbridge) { + d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub) + log.debug "created ${d.displayName} with id ${d.deviceNetworkId}" + def childDevice = getChildDevice(d.deviceNetworkId) + childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber) + if (vbridge.value.ip && vbridge.value.port) { + if (vbridge.value.ip.contains(".")) + childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port) + else + childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port)) + } else + childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress)) + } + } else { + log.debug "found ${d.displayName} with id $selectedHue already exists" + } + } +} + + +def locationHandler(evt) { + def description = evt.description + log.trace "Location: $description" + + def hub = evt?.hubId + def parsedEvent = parseLanMessage(description) + parsedEvent << ["hub":hub] + + if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1")) { + //SSDP DISCOVERY EVENTS + log.trace "SSDP DISCOVERY EVENTS" + def bridges = getHueBridges() + log.trace bridges.toString() + if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) { + //bridge does not exist + log.trace "Adding bridge ${parsedEvent.ssdpUSN}" + bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } else { + // update the values + def ip = convertHexToIP(parsedEvent.networkAddress) + def port = convertHexToInt(parsedEvent.deviceAddress) + def host = ip + ":" + port + log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host." + def dstate = bridges."${parsedEvent.ssdpUSN.toString()}" + def dni = "${parsedEvent.mac}" + def d = getChildDevice(dni) + def networkAddress = null + if (!d) { + childDevices.each { + if (it.getDeviceDataByName("mac")) { + def newDNI = "${it.getDeviceDataByName("mac")}" + if (newDNI != it.deviceNetworkId) { + def oldDNI = it.deviceNetworkId + log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" + it.setDeviceNetworkId("${newDNI}") + if (oldDNI == selectedHue) + app.updateSetting("selectedHue", newDNI) + doDeviceSync() + } + } + } + } else { + networkAddress = d.latestState('networkAddress').stringValue + log.trace "Host: $host - $networkAddress" + if(host != networkAddress) { + log.debug "Device's port or ip changed for device $d..." + dstate.ip = ip + dstate.port = port + dstate.name = "Philips hue ($ip)" + d.sendEvent(name:"networkAddress", value: host) + } + } + } + } + else if (parsedEvent.headers && parsedEvent.body) { + log.trace "HUE BRIDGE RESPONSES" + def headerString = parsedEvent.headers.toString() + if (headerString?.contains("xml")) { + log.trace "description.xml response (application/xml)" + def body = new XmlSlurper().parseText(parsedEvent.body) + if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) { + def bridges = getHueBridges() + def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())} + if (bridge) { + bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true] + } else { + log.error "/description.xml returned a bridge that didn't exist" + } + } + } else if(headerString?.contains("json")) { + log.trace "description.xml response (application/json)" + def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body) + if (body.success != null) { + if (body.success[0] != null) { + if (body.success[0].username) + state.username = body.success[0].username + } + } else if (body.error != null) { + //TODO: handle retries... + log.error "ERROR: application/json ${body.error}" + } else { + //GET /api/${state.username}/lights response (application/json) + if (!body?.state?.on) { //check if first time poll made it here by mistake + def bulbs = getHueBulbs() + log.debug "Adding bulbs to state!" + body.each { k,v -> + bulbs[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub] + } + } + } + } + } else { + log.trace "NON-HUE EVENT $evt.description" + } +} + +def doDeviceSync(){ + log.trace "Doing Hue Device Sync!" + + //shrink the large bulb lists + convertBulbListToMap() + + poll() + + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + discoverBridges() +} + +///////////////////////////////////// +//CHILD DEVICE METHODS +///////////////////////////////////// + +def parse(childDevice, description) { + def parsedEvent = parseLanMessage(description) + if (parsedEvent.headers && parsedEvent.body) { + def headerString = parsedEvent.headers.toString() + if (headerString?.contains("json")) { + def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body) + if (body instanceof java.util.HashMap) + { //poll response + def bulbs = getChildDevices() + //for each bulb + for (bulb in body) { + def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} + if (d) { + if (bulb.value.state?.reachable) { + sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"]) + sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)]) + if (bulb.value.state.sat) { + def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int + def sat = Math.round(bulb.value.state.sat * 100 / 255) as int + def hex = colorUtil.hslToHex(hue, sat) + sendEvent(d.deviceNetworkId, [name: "color", value: hex]) + sendEvent(d.deviceNetworkId, [name: "hue", value: hue]) + sendEvent(d.deviceNetworkId, [name: "saturation", value: sat]) + } + } else { + sendEvent(d.deviceNetworkId, [name: "switch", value: "off"]) + sendEvent(d.deviceNetworkId, [name: "level", value: 100]) + if (bulb.value.state.sat) { + def hue = 23 + def sat = 56 + def hex = colorUtil.hslToHex(23, 56) + sendEvent(d.deviceNetworkId, [name: "color", value: hex]) + sendEvent(d.deviceNetworkId, [name: "hue", value: hue]) + sendEvent(d.deviceNetworkId, [name: "saturation", value: sat]) + } + } + } + + } + } + else + { //put response + def hsl = [:] + body.each { payload -> + log.debug $payload + if (payload?.success) + { + def childDeviceNetworkId = app.id + "/" + def eventType + body?.success[0].each { k,v -> + childDeviceNetworkId += k.split("/")[2] + if (!hsl[childDeviceNetworkId]) hsl[childDeviceNetworkId] = [:] + eventType = k.split("/")[4] + log.debug "eventType: $eventType" + switch(eventType) { + case "on": + sendEvent(childDeviceNetworkId, [name: "switch", value: (v == true) ? "on" : "off"]) + break + case "bri": + sendEvent(childDeviceNetworkId, [name: "level", value: Math.round(v * 100 / 255)]) + break + case "sat": + hsl[childDeviceNetworkId].saturation = Math.round(v * 100 / 255) as int + break + case "hue": + hsl[childDeviceNetworkId].hue = Math.min(Math.round(v * 100 / 65535), 65535) as int + break + } + } + + } + else if (payload.error) + { + log.debug "JSON error - ${body?.error}" + } + + } + + hsl.each { childDeviceNetworkId, hueSat -> + if (hueSat.hue && hueSat.saturation) { + def hex = colorUtil.hslToHex(hueSat.hue, hueSat.saturation) + log.debug "sending ${hueSat} for ${childDeviceNetworkId} as ${hex}" + sendEvent(hsl.childDeviceNetworkId, [name: "color", value: hex]) + } + } + + } + } + } else { + log.debug "parse - got something other than headers,body..." + return [] + } +} + +def on(childDevice, transition = 4) { + log.debug "Executing 'on'" + // Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery + def percent = childDevice.device?.currentValue("level") as Integer ?: 0 + def level = Math.min(Math.round(percent * 255 / 100), 255) + put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition]) + return "level: $percent" +} + +def off(childDevice, transition = 4) { + log.debug "Executing 'off'" + put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition]) +} + +def setLevel(childDevice, percent) { + log.debug "Executing 'setLevel'" + def level = Math.min(Math.round(percent * 255 / 100), 255) + put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0]) +} + +def setSaturation(childDevice, percent) { + log.debug "Executing 'setSaturation($percent)'" + def level = Math.min(Math.round(percent * 255 / 100), 255) + put("lights/${getId(childDevice)}/state", [sat: level]) +} + +def setHue(childDevice, percent) { + log.debug "Executing 'setHue($percent)'" + def level = Math.min(Math.round(percent * 65535 / 100), 65535) + put("lights/${getId(childDevice)}/state", [hue: level]) +} + +def setColor(childDevice, color, alert = "none", transition = 4) { + log.debug "Executing 'setColor($color)'" + def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535) + def sat = Math.min(Math.round(color.saturation * 255 / 100), 255) + + def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition] + if (color.level != null) { + value.bri = Math.min(Math.round(color.level * 255 / 100), 255) + value.on = value.bri > 0 + } + + if (color.switch) { + value.on = color.switch == "on" + } + + log.debug "sending command $value" + put("lights/${getId(childDevice)}/state", value) +} + +def nextLevel(childDevice) { + def level = device.latestValue("level") as Integer ?: 0 + if (level < 100) { + level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer + } + else { + level = 25 + } + setLevel(childDevice,level) +} + +private getId(childDevice) { + if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) { + return childDevice.device?.deviceNetworkId[3..-1] + } + else { + return childDevice.device?.deviceNetworkId.split("/")[-1] + } +} + +private poll() { + def host = getBridgeIP() + def uri = "/api/${state.username}/lights/" + log.debug "GET: $host$uri" + sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1 +HOST: ${host} + +""", physicalgraph.device.Protocol.LAN, selectedHue)) +} + +private put(path, body) { + def host = getBridgeIP() + def uri = "/api/${state.username}/$path" + def bodyJSON = new groovy.json.JsonBuilder(body).toString() + def length = bodyJSON.getBytes().size().toString() + + log.debug "PUT: $host$uri" + log.debug "BODY: ${bodyJSON}" + + sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1 +HOST: ${host} +Content-Length: ${length} + +${bodyJSON} +""", physicalgraph.device.Protocol.LAN, "${selectedHue}")) + +} + +private getBridgeIP() { + def host = null + if (selectedHue) { + def d = getChildDevice(dni) + if (d) + host = d.latestState('networkAddress').stringValue + if (host == null || host == "") { + def serialNumber = selectedHue + def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value + if (bridge?.ip && bridge?.port) { + if (bridge?.ip.contains(".")) + host = "${bridge?.ip}:${bridge?.port}" + else + host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}" + } else if (bridge?.networkAddress && bridge?.deviceAddress) + host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}" + } + log.trace "Bridge: $selectedHue - Host: $host" + } + return host +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +def convertBulbListToMap() { + try { + if (state.bulbs instanceof java.util.List) { + def map = [:] + state.bulbs.unique {it.id}.each { bulb -> + map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]] + } + state.bulbs = map + } + } + catch(Exception e) { + log.error "Caught error attempting to convert bulb list to map: $e" + } +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private Boolean canInstallLabs() { + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) { + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() { + return location.hubs*.firmwareVersionString.findAll { it } +} diff --git a/smartapps/smartthings/hue-mood-lighting.src/hue-mood-lighting.groovy b/smartapps/smartthings/hue-mood-lighting.src/hue-mood-lighting.groovy new file mode 100644 index 00000000000..0fe42bed428 --- /dev/null +++ b/smartapps/smartthings/hue-mood-lighting.src/hue-mood-lighting.groovy @@ -0,0 +1,339 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Hue Mood Lighting + * + * Author: SmartThings + * * + * Date: 2014-02-21 + */ +definition( + name: "Hue Mood Lighting", + namespace: "smartthings", + author: "SmartThings", + description: "Sets the colors and brightness level of your Philips Hue lights to match your mood.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png" +) + +preferences { + page(name: "mainPage", title: "Adjust the color of your Hue lights to match your mood.", install: true, uninstall: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Set the lighting mood when..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + section(anythingSet ? "Select additional mood lighting triggers" : "Set the lighting mood when...", hideable: anythingSet, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section("Control these bulbs...") { + input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required:true, multiple:true + } + section("Choose light effects...") + { + input "color", "enum", title: "Hue Color?", required: false, multiple:false, options: [ + ["Soft White":"Soft White - Default"], + ["White":"White - Concentrate"], + ["Daylight":"Daylight - Energize"], + ["Warm White":"Warm White - Relax"], + "Red","Green","Blue","Yellow","Orange","Purple","Pink"] + input "lightLevel", "enum", title: "Light Level?", required: false, options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]] + } + + section("More options", hideable: true, hidden: true) { + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt=null) { + log.trace "Executing Mood Lighting" + if (allOk) { + log.trace "allOk" + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + log.trace "scheduledTimeHandler()" + eventHandler() +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } + + def hueColor = 0 + def saturation = 100 + + switch(color) { + case "White": + hueColor = 52 + saturation = 19 + break; + case "Daylight": + hueColor = 53 + saturation = 91 + break; + case "Soft White": + hueColor = 23 + saturation = 56 + break; + case "Warm White": + hueColor = 20 + saturation = 80 //83 + break; + case "Blue": + hueColor = 70 + break; + case "Green": + hueColor = 39 + break; + case "Yellow": + hueColor = 25 + break; + case "Orange": + hueColor = 10 + break; + case "Purple": + hueColor = 75 + break; + case "Pink": + hueColor = 83 + break; + case "Red": + hueColor = 100 + break; + } + + state.previous = [:] + + hues.each { + state.previous[it.id] = [ + "switch": it.currentValue("switch"), + "level" : it.currentValue("level"), + "hue": it.currentValue("hue"), + "saturation": it.currentValue("saturation") + ] + } + + log.debug "current values = $state.previous" + + def newValue = [hue: hueColor, saturation: saturation, level: lightLevel as Integer ?: 100] + log.debug "new value = $newValue" + + hues*.setColor(newValue) +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + + +private oncePerDayOk(Long lastTime) { + def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result - $lastTime" + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize diff --git a/smartapps/smartthings/ifttt.src/ifttt.groovy b/smartapps/smartthings/ifttt.src/ifttt.groovy new file mode 100644 index 00000000000..0bc3b20fd55 --- /dev/null +++ b/smartapps/smartthings/ifttt.src/ifttt.groovy @@ -0,0 +1,230 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * IFTTT API Access Application + * + * Author: SmartThings + * + * ---------------------+----------------+--------------------------+------------------------------------ + * Device Type | Attribute Name | Commands | Attribute Values + * ---------------------+----------------+--------------------------+------------------------------------ + * switches | switch | on, off | on, off + * motionSensors | motion | | active, inactive + * contactSensors | contact | | open, closed + * presenceSensors | presence | | present, 'not present' + * temperatureSensors | temperature | | + * accelerationSensors | acceleration | | active, inactive + * waterSensors | water | | wet, dry + * lightSensors | illuminance | | + * humiditySensors | humidity | | + * alarms | alarm | strobe, siren, both, off | strobe, siren, both, off + * locks | lock | lock, unlock | locked, unlocked + * ---------------------+----------------+--------------------------+------------------------------------ + */ + +definition( + name: "IFTTT", + namespace: "smartthings", + author: "SmartThings", + description: "Put the internet to work for you.", + category: "SmartThings Internal", + iconUrl: "https://ifttt.com/images/channels/ifttt.png", + iconX2Url: "https://ifttt.com/images/channels/ifttt_med.png", + oauth: [displayName: "IFTTT", displayLink: "https://ifttt.com"] +) + +preferences { + section("Allow IFTTT to control these things...") { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false + input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false + input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false + input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false + input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false + input "waterSensors", "capability.waterSensor", title: "Which Water Sensors?", multiple: true, required: false + input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false + input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false + input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + } +} + +mappings { + + path("/:deviceType") { + action: [ + GET: "list" + ] + } + path("/:deviceType/states") { + action: [ + GET: "listStates" + ] + } + path("/:deviceType/subscription") { + action: [ + POST: "addSubscription" + ] + } + path("/:deviceType/subscriptions/:id") { + action: [ + DELETE: "removeSubscription" + ] + } + path("/:deviceType/:id") { + action: [ + GET: "show", + PUT: "update" + ] + } + path("/subscriptions") { + action: [ + GET: "listSubscriptions" + ] + } +} + +def installed() { + log.debug settings +} + +def updated() { + log.debug settings +} + +def list() { + log.debug "[PROD] list, params: ${params}" + def type = params.deviceType + settings[type]?.collect{deviceItem(it)} ?: [] +} + +def listStates() { + log.debug "[PROD] states, params: ${params}" + def type = params.deviceType + def attributeName = attributeFor(type) + settings[type]?.collect{deviceState(it, it.currentState(attributeName))} ?: [] +} + +def listSubscriptions() { + state +} + +def update() { + def type = params.deviceType + def data = request.JSON + def devices = settings[type] + def command = data.command + + log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}" + if (command) { + def device = devices?.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + device."$command"() + } + } +} + +def show() { + def type = params.deviceType + def devices = settings[type] + def device = devices.find { it.id == params.id } + + log.debug "[PROD] show, params: ${params}, devices: ${devices*.id}" + if (!device) { + httpError(404, "Device not found") + } + else { + def attributeName = attributeFor(type) + def s = device.currentState(attributeName) + deviceState(device, s) + } +} + +def addSubscription() { + log.debug "[PROD] addSubscription1" + def type = params.deviceType + def data = request.JSON + def attribute = attributeFor(type) + def devices = settings[type] + def deviceId = data.deviceId + def callbackUrl = data.callbackUrl + def device = devices.find { it.id == deviceId } + + log.debug "[PROD] addSubscription, params: ${params}, request: ${data}, device: ${device}" + if (device) { + log.debug "Adding switch subscription " + callbackUrl + state[deviceId] = [callbackUrl: callbackUrl] + subscribe(device, attribute, deviceHandler) + } + log.info state + +} + +def removeSubscription() { + def type = params.deviceType + def devices = settings[type] + def deviceId = params.id + def device = devices.find { it.id == deviceId } + + log.debug "[PROD] removeSubscription, params: ${params}, request: ${data}, device: ${device}" + if (device) { + log.debug "Removing $device.displayName subscription" + state.remove(device.id) + unsubscribe(device) + } + log.info state +} + +def deviceHandler(evt) { + def deviceInfo = state[evt.deviceId] + if (deviceInfo) { + try { + httpPostJson(uri: deviceInfo.callbackUrl, path: '', body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]) { + log.debug "[PROD IFTTT] Event data successfully posted" + } + } catch (groovyx.net.http.ResponseParseException e) { + log.debug("Error parsing ifttt payload ${e}") + } + } else { + log.debug "[PROD] No subscribed device found" + } +} + +private deviceItem(it) { + it ? [id: it.id, label: it.displayName] : null +} + +private deviceState(device, s) { + device && s ? [id: device.id, label: device.displayName, name: s.name, value: s.value, unixTime: s.date.time] : null +} + +private attributeFor(type) { + switch (type) { + case "switches": + log.debug "[PROD] switch type" + return "switch" + case "locks": + log.debug "[PROD] lock type" + return "lock" + case "alarms": + log.debug "[PROD] alarm type" + return "alarm" + case "lightSensors": + log.debug "[PROD] illuminance type" + return "illuminance" + default: + log.debug "[PROD] other sensor type" + return type - "Sensors" + } +} diff --git a/smartapps/smartthings/it-moved.src/it-moved.groovy b/smartapps/smartthings/it-moved.src/it-moved.groovy new file mode 100644 index 00000000000..1023807548e --- /dev/null +++ b/smartapps/smartthings/it-moved.src/it-moved.groovy @@ -0,0 +1,67 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * It Moved + * + * Author: SmartThings + */ +definition( + name: "It Moved", + namespace: "smartthings", + author: "SmartThings", + description: "Send a text when movement is detected", + category: "Fun & Social", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer@2x.png" +) + +preferences { + section("When movement is detected...") { + input "accelerationSensor", "capability.accelerationSensor", title: "Where?" + } + section("Text me at...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + +def installed() { + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def updated() { + unsubscribe() + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def accelerationActiveHandler(evt) { + // Don't send a continuous stream of text messages + def deltaSeconds = 5 + def timeAgo = new Date(now() - (1000 * deltaSeconds)) + def recentEvents = accelerationSensor.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds" + def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 + + if (alreadySentSms) { + log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + } else { + if (location.contactBookEnabled) { + log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}" + sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients) + } + else { + log.debug "$accelerationSensor has moved, texting $phone1" + sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved") + } + } +} diff --git a/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy b/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy new file mode 100644 index 00000000000..0e66cc33c36 --- /dev/null +++ b/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy @@ -0,0 +1,100 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * It's Too Cold + * + * Author: SmartThings + */ +definition( + name: "It's Too Cold", + namespace: "smartthings", + author: "SmartThings", + description: "Monitor the temperature and when it drops below your setting get a text and/or turn on a heater or additional appliance.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png" +) + +preferences { + section("Monitor the temperature...") { + input "temperatureSensor1", "capability.temperatureMeasurement" + } + section("When the temperature drops below...") { + input "temperature1", "number", title: "Temperature?" + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phone1", "phone", title: "Send a Text Message?", required: false + } + } + section("Turn on a heater...") { + input "switch1", "capability.switch", required: false + } +} + +def installed() { + subscribe(temperatureSensor1, "temperature", temperatureHandler) +} + +def updated() { + unsubscribe() + subscribe(temperatureSensor1, "temperature", temperatureHandler) +} + +def temperatureHandler(evt) { + log.trace "temperature: $evt.value, $evt" + + def tooCold = temperature1 + def mySwitch = settings.switch1 + + // TODO: Replace event checks with internal state (the most reliable way to know if an SMS has been sent recently or not). + if (evt.doubleValue <= tooCold) { + log.debug "Checking how long the temperature sensor has been reporting <= $tooCold" + + // Don't send a continuous stream of text messages + def deltaMinutes = 10 // TODO: Ask for "retry interval" in prefs? + def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong()) + def recentEvents = temperatureSensor1.eventsSince(timeAgo)?.findAll { it.name == "temperature" } + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes" + def alreadySentSms = recentEvents.count { it.doubleValue <= tooCold } > 1 + + if (alreadySentSms) { + log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes" + // TODO: Send "Temperature back to normal" SMS, turn switch off + } else { + log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch" + send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:"F"}") + switch1?.on() + } + } +} + +private send(msg) { + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phone1) { + log.debug("sending text message") + sendSms(phone1, msg) + } + } + + log.debug msg +} diff --git a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy new file mode 100644 index 00000000000..e9c8c0f3d33 --- /dev/null +++ b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy @@ -0,0 +1,100 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * It's Too Hot + * + * Author: SmartThings + */ +definition( + name: "It's Too Hot", + namespace: "smartthings", + author: "SmartThings", + description: "Monitor the temperature and when it rises above your setting get a notification and/or turn on an A/C unit or fan.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/its-too-hot.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/its-too-hot@2x.png" +) + +preferences { + section("Monitor the temperature...") { + input "temperatureSensor1", "capability.temperatureMeasurement" + } + section("When the temperature rises above...") { + input "temperature1", "number", title: "Temperature?" + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phone1", "phone", title: "Send a Text Message?", required: false + } + } + section("Turn on which A/C or fan...") { + input "switch1", "capability.switch", required: false + } +} + +def installed() { + subscribe(temperatureSensor1, "temperature", temperatureHandler) +} + +def updated() { + unsubscribe() + subscribe(temperatureSensor1, "temperature", temperatureHandler) +} + +def temperatureHandler(evt) { + log.trace "temperature: $evt.value, $evt" + + def tooHot = temperature1 + def mySwitch = settings.switch1 + + // TODO: Replace event checks with internal state (the most reliable way to know if an SMS has been sent recently or not). + if (evt.doubleValue >= tooHot) { + log.debug "Checking how long the temperature sensor has been reporting <= $tooHot" + + // Don't send a continuous stream of text messages + def deltaMinutes = 10 // TODO: Ask for "retry interval" in prefs? + def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong()) + def recentEvents = temperatureSensor1.eventsSince(timeAgo)?.findAll { it.name == "temperature" } + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes" + def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1 + + if (alreadySentSms) { + log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes" + // TODO: Send "Temperature back to normal" SMS, turn switch off + } else { + log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch" + send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:"F"}") + switch1?.on() + } + } +} + +private send(msg) { + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phone1) { + log.debug("sending text message") + sendSms(phone1, msg) + } + } + + log.debug msg +} diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy b/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy new file mode 100644 index 00000000000..c4b3b8e3bbc --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy @@ -0,0 +1,123 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Keep Me Cozy II + * + * Author: SmartThings + */ + +definition( + name: "Keep Me Cozy II", + namespace: "smartthings", + author: "SmartThings", + description: "Works the same as Keep Me Cozy, but enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png" +) + +preferences() { + section("Choose thermostat... ") { + input "thermostat", "capability.thermostat" + } + section("Heat setting..." ) { + input "heatingSetpoint", "decimal", title: "Degrees" + } + section("Air conditioning setting...") { + input "coolingSetpoint", "decimal", title: "Degrees" + } + section("Optionally choose temperature sensor to use instead of the thermostat's... ") { + input "sensor", "capability.temperatureMeasurement", title: "Temp Sensors", required: false + } +} + +def installed() +{ + log.debug "enter installed, state: $state" + subscribeToEvents() +} + +def updated() +{ + log.debug "enter updated, state: $state" + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() +{ + subscribe(location, changedLocationMode) + if (sensor) { + subscribe(sensor, "temperature", temperatureHandler) + subscribe(thermostat, "temperature", temperatureHandler) + subscribe(thermostat, "thermostatMode", temperatureHandler) + } + evaluate() +} + +def changedLocationMode(evt) +{ + log.debug "changedLocationMode mode: $evt.value, heat: $heat, cool: $cool" + evaluate() +} + +def temperatureHandler(evt) +{ + evaluate() +} + +private evaluate() +{ + if (sensor) { + def threshold = 1.0 + def tm = thermostat.currentThermostatMode + def ct = thermostat.currentTemperature + def currentTemp = sensor.currentTemperature + log.trace("evaluate:, mode: $tm -- temp: $ct, heat: $thermostat.currentHeatingSetpoint, cool: $thermostat.currentCoolingSetpoint -- " + + "sensor: $currentTemp, heat: $heatingSetpoint, cool: $coolingSetpoint") + if (tm in ["cool","auto"]) { + // air conditioner + if (currentTemp - coolingSetpoint >= threshold) { + thermostat.setCoolingSetpoint(ct - 2) + log.debug "thermostat.setCoolingSetpoint(${ct - 2}), ON" + } + else if (coolingSetpoint - currentTemp >= threshold && ct - thermostat.currentCoolingSetpoint >= threshold) { + thermostat.setCoolingSetpoint(ct + 2) + log.debug "thermostat.setCoolingSetpoint(${ct + 2}), OFF" + } + } + if (tm in ["heat","emergency heat","auto"]) { + // heater + if (heatingSetpoint - currentTemp >= threshold) { + thermostat.setHeatingSetpoint(ct + 2) + log.debug "thermostat.setHeatingSetpoint(${ct + 2}), ON" + } + else if (currentTemp - heatingSetpoint >= threshold && thermostat.currentHeatingSetpoint - ct >= threshold) { + thermostat.setHeatingSetpoint(ct - 2) + log.debug "thermostat.setHeatingSetpoint(${ct - 2}), OFF" + } + } + } + else { + thermostat.setHeatingSetpoint(heatingSetpoint) + thermostat.setCoolingSetpoint(coolingSetpoint) + thermostat.poll() + } +} + +// for backward compatibility with existing subscriptions +def coolingSetpointHandler(evt) { + log.debug "coolingSetpointHandler()" +} +def heatingSetpointHandler (evt) { + log.debug "heatingSetpointHandler ()" +} diff --git a/smartapps/smartthings/keep-me-cozy.src/keep-me-cozy.groovy b/smartapps/smartthings/keep-me-cozy.src/keep-me-cozy.groovy new file mode 100644 index 00000000000..8a8444ebe3a --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy.src/keep-me-cozy.groovy @@ -0,0 +1,95 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Keep Me Cozy + * + * Author: SmartThings + */ +definition( + name: "Keep Me Cozy", + namespace: "smartthings", + author: "SmartThings", + description: "Changes your thermostat settings automatically in response to a mode change. Often used with Bon Voyage, Rise and Shine, and other Mode Magic SmartApps to automatically keep you comfortable while you're present and save you energy and money while you are away.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png" +) + +preferences { + section("Choose thermostat... ") { + input "thermostat", "capability.thermostat" + } + section("Heat setting...") { + input "heatingSetpoint", "number", title: "Degrees?" + } + section("Air conditioning setting..."){ + input "coolingSetpoint", "number", title: "Degrees?" + } +} + +def installed() +{ + subscribe(thermostat, "heatingSetpoint", heatingSetpointHandler) + subscribe(thermostat, "coolingSetpoint", coolingSetpointHandler) + subscribe(thermostat, "temperature", temperatureHandler) + subscribe(location, changedLocationMode) + subscribe(app, appTouch) +} + +def updated() +{ + unsubscribe() + subscribe(thermostat, "heatingSetpoint", heatingSetpointHandler) + subscribe(thermostat, "coolingSetpoint", coolingSetpointHandler) + subscribe(thermostat, "temperature", temperatureHandler) + subscribe(location, changedLocationMode) + subscribe(app, appTouch) +} + +def heatingSetpointHandler(evt) +{ + log.debug "heatingSetpoint: $evt, $settings" +} + +def coolingSetpointHandler(evt) +{ + log.debug "coolingSetpoint: $evt, $settings" +} + +def temperatureHandler(evt) +{ + log.debug "currentTemperature: $evt, $settings" +} + +def changedLocationMode(evt) +{ + log.debug "changedLocationMode: $evt, $settings" + + thermostat.setHeatingSetpoint(heatingSetpoint) + thermostat.setCoolingSetpoint(coolingSetpoint) + thermostat.poll() +} + +def appTouch(evt) +{ + log.debug "appTouch: $evt, $settings" + + thermostat.setHeatingSetpoint(heatingSetpoint) + thermostat.setCoolingSetpoint(coolingSetpoint) + thermostat.poll() +} + +// catchall +def event(evt) +{ + log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}" +} diff --git a/smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy b/smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy new file mode 100644 index 00000000000..b47f657f3be --- /dev/null +++ b/smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy @@ -0,0 +1,182 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Laundry Monitor + * + * Author: SmartThings + * + * Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done. + * + * Date: 2013-02-21 + */ + +definition( + name: "Laundry Monitor", + namespace: "smartthings", + author: "SmartThings", + description: "Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner%402x.png" +) + +preferences { + section("Tell me when this washer/dryer has stopped..."){ + input "sensor1", "capability.accelerationSensor" + } + section("Via this number (optional, sends push notification if not specified)"){ + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number", required: false + } + } + section("And by turning on these lights (optional)") { + input "switches", "capability.switch", required: false, multiple: true, title: "Which lights?" + input "lightMode", "enum", options: ["Flash Lights", "Turn On Lights"], required: false, defaultValue: "Turn On Lights", title: "Action?" + } + section("Time thresholds (in minutes, optional)"){ + input "cycleTime", "decimal", title: "Minimum cycle time", required: false, defaultValue: 10 + input "fillTime", "decimal", title: "Time to fill tub", required: false, defaultValue: 5 + } +} + +def installed() +{ + initialize() +} + +def updated() +{ + unsubscribe() + initialize() +} + +def initialize() { + subscribe(sensor1, "acceleration.active", accelerationActiveHandler) + subscribe(sensor1, "acceleration.inactive", accelerationInactiveHandler) +} + +def accelerationActiveHandler(evt) { + log.trace "vibration" + if (!state.isRunning) { + log.info "Arming detector" + state.isRunning = true + state.startedAt = now() + } + state.stoppedAt = null +} + +def accelerationInactiveHandler(evt) { + log.trace "no vibration, isRunning: $state.isRunning" + if (state.isRunning) { + log.debug "startedAt: ${state.startedAt}, stoppedAt: ${state.stoppedAt}" + if (!state.stoppedAt) { + state.stoppedAt = now() + def delay = Math.floor(fillTime * 60).toInteger() + runIn(delay, checkRunning, [overwrite: false]) + } + } +} + +def checkRunning() { + log.trace "checkRunning()" + if (state.isRunning) { + def fillTimeMsec = fillTime ? fillTime * 60000 : 300000 + def sensorStates = sensor1.statesSince("acceleration", new Date((now() - fillTimeMsec) as Long)) + + if (!sensorStates.find{it.value == "active"}) { + + def cycleTimeMsec = cycleTime ? cycleTime * 60000 : 600000 + def duration = now() - state.startedAt + if (duration - fillTimeMsec > cycleTimeMsec) { + log.debug "Sending notification" + + def msg = "${sensor1.displayName} is finished" + log.info msg + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + + if (phone) { + sendSms phone, msg + } else { + sendPush msg + } + + } + + if (switches) { + if (lightMode?.equals("Turn On Lights")) { + switches.on() + } else { + flashLights() + } + } + } else { + log.debug "Not sending notification because machine wasn't running long enough $duration versus $cycleTimeMsec msec" + } + state.isRunning = false + log.info "Disarming detector" + } else { + log.debug "skipping notification because vibration detected again" + } + } + else { + log.debug "machine no longer running" + } +} + +private flashLights() { + def doFlash = true + def onFor = onFor ?: 1000 + def offFor = offFor ?: 1000 + def numFlashes = numFlashes ?: 3 + + log.debug "LAST ACTIVATED IS: ${state.lastActivated}" + if (state.lastActivated) { + def elapsed = now() - state.lastActivated + def sequenceTime = (numFlashes + 1) * (onFor + offFor) + doFlash = elapsed > sequenceTime + log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}" + } + + if (doFlash) { + log.debug "FLASHING $numFlashes times" + state.lastActivated = now() + log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}" + def initialActionOn = switches.collect{it.currentSwitch != "on"} + def delay = 1L + numFlashes.times { + log.trace "Switch on after $delay msec" + switches.eachWithIndex {s, i -> + if (initialActionOn[i]) { + s.on(delay: delay) + } + else { + s.off(delay:delay) + } + } + delay += onFor + log.trace "Switch off after $delay msec" + switches.eachWithIndex {s, i -> + if (initialActionOn[i]) { + s.off(delay: delay) + } + else { + s.on(delay:delay) + } + } + delay += offFor + } + } +} diff --git a/smartapps/smartthings/left-it-open.src/left-it-open.groovy b/smartapps/smartthings/left-it-open.src/left-it-open.groovy new file mode 100644 index 00000000000..43732f09ef1 --- /dev/null +++ b/smartapps/smartthings/left-it-open.src/left-it-open.groovy @@ -0,0 +1,110 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Left It Open + * + * Author: SmartThings + * Date: 2013-05-09 + */ +definition( + name: "Left It Open", + namespace: "smartthings", + author: "SmartThings", + description: "Notifies you when you have left a door or window open longer that a specified amount of time.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/bon-voyage.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/bon-voyage%402x.png" +) + +preferences { + + section("Monitor this door or window") { + input "contact", "capability.contactSensor" + } + section("And notify me if it's open for more than this many minutes (default 10)") { + input "openThreshold", "number", description: "Number of minutes", required: false + } + section("Delay between notifications (default 10 minutes") { + input "frequency", "number", title: "Number of minutes", description: "", required: false + } + section("Via text message at this number (or via push notification if not specified") { + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone number (optional)", required: false + } + } +} + +def installed() { + log.trace "installed()" + subscribe() +} + +def updated() { + log.trace "updated()" + unsubscribe() + subscribe() +} + +def subscribe() { + subscribe(contact, "contact.open", doorOpen) + subscribe(contact, "contact.closed", doorClosed) +} + +def doorOpen(evt) +{ + log.trace "doorOpen($evt.name: $evt.value)" + def t0 = now() + def delay = (openThreshold != null && openThreshold != "") ? openThreshold * 60 : 600 + runIn(delay, doorOpenTooLong, [overwrite: false]) + log.debug "scheduled doorOpenTooLong in ${now() - t0} msec" +} + +def doorClosed(evt) +{ + log.trace "doorClosed($evt.name: $evt.value)" +} + +def doorOpenTooLong() { + def contactState = contact.currentState("contact") + def freq = (frequency != null && frequency != "") ? frequency * 60 : 600 + + if (contactState.value == "open") { + def elapsed = now() - contactState.rawDateCreated.time + def threshold = ((openThreshold != null && openThreshold != "") ? openThreshold * 60000 : 60000) - 1000 + if (elapsed >= threshold) { + log.debug "Contact has stayed open long enough since last check ($elapsed ms): calling sendMessage()" + sendMessage() + runIn(freq, doorOpenTooLong, [overwrite: false]) + } else { + log.debug "Contact has not stayed open long enough since last check ($elapsed ms): doing nothing" + } + } else { + log.warn "doorOpenTooLong() called but contact is closed: doing nothing" + } +} + +void sendMessage() +{ + def minutes = (openThreshold != null && openThreshold != "") ? openThreshold : 10 + def msg = "${contact.displayName} has been left open for ${minutes} minutes." + log.info msg + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (phone) { + sendSms phone, msg + } else { + sendPush msg + } + } +} diff --git a/smartapps/smartthings/let-there-be-light.src/let-there-be-light.groovy b/smartapps/smartthings/let-there-be-light.src/let-there-be-light.groovy new file mode 100644 index 00000000000..eecff6c66d9 --- /dev/null +++ b/smartapps/smartthings/let-there-be-light.src/let-there-be-light.groovy @@ -0,0 +1,53 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Let There Be Light! + * Turn your lights on when an open/close sensor opens and off when the sensor closes. + * + * Author: SmartThings + */ +definition( + name: "Let There Be Light!", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights on when a SmartSense Multi is opened and turn them off when it is closed.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png" +) + +preferences { + section("When the door opens/closes...") { + input "contact1", "capability.contactSensor", title: "Where?" + } + section("Turn on/off a light...") { + input "switch1", "capability.switch" + } +} + +def installed() { + subscribe(contact1, "contact", contactHandler) +} + +def updated() { + unsubscribe() + subscribe(contact1, "contact", contactHandler) +} + +def contactHandler(evt) { + log.debug "$evt.value" + if (evt.value == "open") { + switch1.on() + } else if (evt.value == "closed") { + switch1.off() + } +} diff --git a/smartapps/smartthings/life360-connect.src/life360-connect.groovy b/smartapps/smartthings/life360-connect.src/life360-connect.groovy new file mode 100644 index 00000000000..eabf5454160 --- /dev/null +++ b/smartapps/smartthings/life360-connect.src/life360-connect.groovy @@ -0,0 +1,768 @@ +/** + * life360 + * + * Copyright 2014 Jeff's Account + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Life360 (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Life360 Service Manager", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png", + oauth: [displayName: "Life360", displayLink: "Life360"] +) { + appSetting "clientId" + appSetting "clientSecret" + appSetting "serverUrl" +} + +preferences { + page(name: "Credentials", title: "Life360 Authentication", content: "authPage", nextPage: "listCirclesPage", install: false) + page(name: "listCirclesPage", title: "Select Life360 Circle", nextPage: "listPlacesPage", content: "listCircles", install: false) + page(name: "listPlacesPage", title: "Select Life360 Place", nextPage: "listUsersPage", content: "listPlaces", install: false) + page(name: "listUsersPage", title: "Select Life360 Users", content: "listUsers", install: true) +} + +// page(name: "Credentials", title: "Enter Life360 Credentials", content: "getCredentialsPage", nextPage: "listCirclesPage", install: false) +// page(name: "page3", title: "Select Life360 Users", content: "listUsers") + +mappings { + + path("/placecallback") { + action: [ + POST: "placeEventHandler", + GET: "placeEventHandler" + ] + } + + path("/receiveToken") { + action: [ + POST: "receiveToken", + GET: "receiveToken" + ] + } +} + +def authPage() +{ + log.debug "authPage()" + + def description = "Life360 Credentials Already Entered." + + def uninstallOption = false + if (app.installationState == "COMPLETE") + uninstallOption = true + + if(!state.life360AccessToken) + { + log.debug "about to create access token" + createAccessToken() + description = "Click to enter Life360 Credentials." + + def redirectUrl = oauthInitUrl() + + log.debug "RedirectURL = ${redirectUrl}" + + return dynamicPage(name: "Credentials", title: "Life360", nextPage:"listCirclesPage", uninstall: uninstallOption, install:false) { + section { + href url:redirectUrl, style:"embedded", required:false, title:"Life360", description:description + } + } + } + else + { + listCircles() + } +} + +def receiveToken() { + + state.life360AccessToken = params.access_token + + def html = """ + + + + +Withings Connection + + + +
+ Life360 icon + connected device icon + SmartThings logo +

Your Life360 Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

+
+ + +""" + + render contentType: 'text/html', data: html + +} + +def oauthInitUrl() +{ + log.debug "oauthInitUrl" + def stcid = getSmartThingsClientId(); + + // def oauth_url = "https://api.life360.com/v3/oauth2/authorize?client_id=pREqugabRetre4EstetherufrePumamExucrEHuc&response_type=token&redirect_uri=http%3A%2F%2Fwww.smartthings.com" + + state.oauthInitState = UUID.randomUUID().toString() + + def oauthParams = [ + response_type: "token", + client_id: stcid, + redirect_uri: buildRedirectUrl() + ] + + return "https://api.life360.com/v3/oauth2/authorize?" + toQueryString(oauthParams) +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +def getSmartThingsClientId() { + return "pREqugabRetre4EstetherufrePumamExucrEHuc" +} + +def getServerUrl() { appSettings.serverUrl } + +def buildRedirectUrl() +{ + log.debug "buildRedirectUrl" + // /api/token/:st_token/smartapps/installations/:id/something + + return serverUrl + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/receiveToken" +} + +// +// This method is no longer used - was part of the initial username/password based authentication that has now been replaced +// by the full OAUTH web flow +// + +def getCredentialsPage() { + + dynamicPage(name: "Credentials", title: "Enter Life360 Credentials", nextPage: "listCirclesPage", uninstall: true, install:false) + { + section("Life 360 Credentials ...") { + input "username", "text", title: "Life360 Username?", multiple: false, required: true + input "password", "password", title: "Life360 Password?", multiple: false, required: true, autoCorrect: false + } + + } + +} + +// +// This method is no longer used - was part of the initial username/password based authentication that has now been replaced +// by the full OAUTH web flow +// + +def getCredentialsErrorPage(String message) { + + dynamicPage(name: "Credentials", title: "Enter Life360 Credentials", nextPage: "listCirclesPage", uninstall: true, install:false) + { + section("Life 360 Credentials ...") { + input "username", "text", title: "Life360 Username?", multiple: false, required: true + input "password", "password", title: "Life360 Password?", multiple: false, required: true, autoCorrect: false + paragraph "${message}" + } + + } + +} + +def testLife360Connection() { + + if (state.life360AccessToken) + true + else + false + +} + +// +// This method is no longer used - was part of the initial username/password based authentication that has now been replaced +// by the full OAUTH web flow +// + +def initializeLife360Connection() { + + def oauthClientId = appSettings.clientId + def oauthClientSecret = appSettings.clientSecret + + log.debug "Installed with settings: ${settings}" + + initialize() + + def username = settings.username + def password = settings.password + + // Base 64 encode the credentials + + def basicCredentials = "${oauthClientId}:${oauthClientSecret}" + def encodedCredentials = basicCredentials.encodeAsBase64().toString() + + log.debug "Encoded Creds: ${encodedCredentials}" + + + // call life360, get OAUTH token using password flow, save + // curl -X POST -H "Authorization: Basic cFJFcXVnYWJSZXRyZTRFc3RldGhlcnVmcmVQdW1hbUV4dWNyRUh1YzptM2ZydXBSZXRSZXN3ZXJFQ2hBUHJFOTZxYWtFZHI0Vg==" + // -F "grant_type=password" -F "username=jeff@hagins.us" -F "password=tondeleo" https://api.life360.com/v3/oauth2/token.json + + + def url = "https://api.life360.com/v3/oauth2/token.json" + + + def postBody = "grant_type=password&" + + "username=${username}&"+ + "password=${password}" + + log.debug "Post Body: ${postBody}" + + def result = null + + try { + + httpPost(uri: url, body: postBody, headers: ["Authorization": "Basic ${encodedCredentials}" ]) {response -> + result = response + } + if (result.data.access_token) { + state.life360AccessToken = result.data.access_token + log.debug "Access Token = ${state.life360AccessToken}" + return true; + } + log.debug "Response=${result.data}" + return false; + + } + catch (e) { + log.debug e + return false; + } + +} + +def listCircles (){ + + // understand whether to present the Uninstall option + def uninstallOption = false + if (app.installationState == "COMPLETE") + uninstallOption = true + + // get connected to life360 api + + if (testLife360Connection()) { + + // now pull back the list of Life360 circles + // curl -X GET -H "Authorization: Bearer MmEzODQxYWQtMGZmMy00MDZhLWEwMGQtMTIzYmYxYzFmNGU3" https://api.life360.com/v3/circles.json + + def url = "https://api.life360.com/v3/circles.json" + + def result = null + + httpGet(uri: url, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response -> + result = response + } + + log.debug "Circles=${result.data}" + + def circles = result.data.circles + + if (circles.size > 1) { + return ( + dynamicPage(name: "listCirclesPage", title: "Life360 Circles", nextPage: null, uninstall: uninstallOption, install:false) { + section("Select Life360 Circle:") { + input "circle", "enum", multiple: false, required:true, title:"Life360 Circle: ", options: circles.collectEntries{[it.id, it.name]} + } + } + ) + } + else { + state.circle = circles[0].id + return (listPlaces()) + } + } + else { + getCredentialsErrorPage("Invalid Usernaname or password.") + } + +} + +def listPlaces() { + + // understand whether to present the Uninstall option + def uninstallOption = false + if (app.installationState == "COMPLETE") + uninstallOption = true + + if (!state?.circle) + state.circle = settings.circle + + // call life360 and get the list of places in the circle + + def url = "https://api.life360.com/v3/circles/${state.circle}/places.json" + + def result = null + + httpGet(uri: url, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response -> + result = response + } + + log.debug "Places=${result.data}" + + def places = result.data.places + state.places = places + + // If there is a place called "Home" use it as the default + def defaultPlace = places.find{it.name=="Home"} + def defaultPlaceId + if (defaultPlace) { + defaultPlaceId = defaultPlace.id + log.debug "Place = $defaultPlace.name, Id=$defaultPlace.id" + } + + dynamicPage(name: "listPlacesPage", title: "Life360 Places", nextPage: null, uninstall: uninstallOption, install:false) { + section("Select Life360 Place to Match Current Location:") { + paragraph "Please select the ONE Life360 Place that matches your SmartThings location: ${location.name}" + input "place", "enum", multiple: false, required:true, title:"Life360 Places: ", options: places.collectEntries{[it.id, it.name]}, defaultValue: defaultPlaceId + } + } + +} + +def listUsers () { + + // understand whether to present the Uninstall option + def uninstallOption = false + if (app.installationState == "COMPLETE") + uninstallOption = true + + if (!state?.circle) + state.circle = settings.circle + + // call life360 and get list of users (members) + + def url = "https://api.life360.com/v3/circles/${state.circle}/members.json" + + def result = null + + httpGet(uri: url, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response -> + result = response + } + + log.debug "Members=${result.data}" + + // save members list for later + + def members = result.data.members + + state.members = members + + // build preferences page + + dynamicPage(name: "listUsersPage", title: "Life360 Users", nextPage: null, uninstall: uninstallOption, install:true) { + section("Select Life360 Users to Import into SmartThings:") { + input "users", "enum", multiple: true, required:true, title:"Life360 Users: ", options: members.collectEntries{[it.id, it.firstName+" "+it.lastName]} + } + } +} + +def installed() { + + if (!state?.circle) + state.circle = settings.circle + + log.debug "In installed() method." + // log.debug "Members: ${state.members}" + // log.debug "Users: ${settings.users}" + + settings.users.each {memberId-> + + // log.debug "Find by Member Id = ${memberId}" + + def member = state.members.find{it.id==memberId} + + // log.debug "After Find Attempt." + + // log.debug "Member Id = ${member.id}, Name = ${member.firstName} ${member.lastName}, Email Address = ${member.loginEmail}" + + // log.debug "External Id=${app.id}:${member.id}" + + // create the device + if (member) { + + def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true]) + + // save the memberId on the device itself so we can find easily later + // childDevice.setMemberId(member.id) + + if (childDevice) + { + // log.debug "Child Device Successfully Created" + generateInitialEvent (member, childDevice) + + // build the icon name form the L360 Avatar URL + // URL Format: https://www.life360.com/img/user_images/b4698717-1f2e-4b7a-b0d4-98ccfb4e9730/Maddie_Hagins_51d2eea2019c7.jpeg + // SmartThings Icon format is: L360.b4698717-1f2e-4b7a-b0d4-98ccfb4e9730.Maddie_Hagins_51d2eea2019c7 + try { + + // build the icon name from the avatar URL + log.debug "Avatar URL = ${member.avatar}" + def urlPathElements = member.avatar.tokenize("/") + def fileElements = urlPathElements[5].tokenize(".") + // def icon = "st.Lighting.light1" + def icon="l360.${urlPathElements[4]}.${fileElements[0]}" + log.debug "Icon = ${icon}" + + // set the icon on the device + childDevice.setIcon("presence","present",icon) + childDevice.setIcon("presence","not present",icon) + childDevice.save() + } + catch (e) { // do nothing + log.debug "Error = ${e}" + } + } + } + } + + createCircleSubscription() + +} + +def createCircleSubscription() { + + // delete any existing webhook subscriptions for this circle + // + // curl -X DELETE https://webhook.qa.life360.com/v3/circles/:circleId/webhook.json + + log.debug "Remove any existing Life360 Webhooks for this Circle." + + def deleteUrl = "https://api.life360.com/v3/circles/${state.circle}/webhook.json" + + try { // ignore any errors - there many not be any existing webhooks + + httpDelete (uri: deleteUrl, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response -> + result = response} + } + + catch (e) { + + log.debug (e) + } + + // subscribe to the life360 webhook to get push notifications on place events within this circle + + // POST /circles/:circle_id/places/webooks + // Params: hook_url + + log.debug "Create a new Life360 Webhooks for this Circle." + + createAccessToken() // create our own OAUTH access token to use in webhook url + + def hookUrl = "${serverUrl}/api/smartapps/installations/${app.id}/placecallback?access_token=${state.accessToken}".encodeAsURL() + + def url = "https://api.life360.com/v3/circles/${state.circle}/webhook.json" + + def postBody = "url=${hookUrl}" + + log.debug "Post Body: ${postBody}" + + def result = null + + try { + + httpPost(uri: url, body: postBody, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response -> + result = response} + + } catch (e) { + log.debug (e) + } + + // response from this call looks like this: + // {"circleId":"41094b6a-32fc-4ef5-a9cd-913f82268836","userId":"0d1db550-9163-471b-8829-80b375e0fa51","clientId":"11", + // "hookUrl":"https://testurl.com"} + + log.debug "Response = ${response}" + + if (result.data?.hookUrl) { + log.debug "Webhook creation successful. Response = ${result.data}" + + } +} + + +def updated() { + + if (!state?.circle) + state.circle = settings.circle + + log.debug "In updated() method." + // log.debug "Members: ${state.members}" + // log.debug "Users: ${settings.users}" + + // loop through selected users and try to find child device for each + + settings.users.each {memberId-> + + def externalId = "${app.id}.${memberId}" + + // find the appropriate child device based on my app id and the device network id + + def deviceWrapper = getChildDevice("${externalId}") + + if (!deviceWrapper) { // device isn't there - so we need to create + + // log.debug "Find by Member Id = ${memberId}" + + def member = state.members.find{it.id==memberId} + + // log.debug "After Find Attempt." + + log.debug "Member Id = ${member.id}, Name = ${member.firstName} ${member.lastName}, Email Address = ${member.loginEmail}" + + // log.debug "External Id=${app.id}:${member.id}" + + // create the device + def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true]) + // childDevice.setMemberId(member.id) + + if (childDevice) + { + // log.debug "Child Device Successfully Created" + generateInitialEvent (member, childDevice) + + // build the icon name form the L360 Avatar URL + // URL Format: https://www.life360.com/img/user_images/b4698717-1f2e-4b7a-b0d4-98ccfb4e9730/Maddie_Hagins_51d2eea2019c7.jpeg + // SmartThings Icon format is: L360.b4698717-1f2e-4b7a-b0d4-98ccfb4e9730.Maddie_Hagins_51d2eea2019c7 + try { + + // build the icon name from the avatar URL + log.debug "Avatar URL = ${member.avatar}" + def urlPathElements = member.avatar.tokenize("/") + def icon="l360.${urlPathElements[4]}.${urlPathElements[5]}" + + // set the icon on the device + childDevice.setIcon("presence","present",icon) + childDevice.setIcon("presence","not present",icon) + childDevice.save() + } + catch (e) { // do nothing + log.debug "Error = ${e}" + } + } + + } + else { + + // log.debug "Find by Member Id = ${memberId}" + + def member = state.members.find{it.id==memberId} + + generateInitialEvent (member, deviceWrapper) + + } + } + + // Now remove any existing devices that represent users that are no longer selected + + def childDevices = getAllChildDevices() + + log.debug "Child Devices = ${childDevices}" + + childDevices.each {childDevice-> + + log.debug "Child = ${childDevice}, DNI=${childDevice.deviceNetworkId}" + + // def childMemberId = childDevice.getMemberId() + + def splitStrings = childDevice.deviceNetworkId.split("\\.") + + log.debug "Strings = ${splitStrings}" + + def childMemberId = splitStrings[1] + + log.debug "Child Member Id = ${childMemberId}" + + log.debug "Settings.users = ${settings.users}" + + if (!settings.users.find{it==childMemberId}) { + deleteChildDevice(childDevice.deviceNetworkId) + def member = state.members.find {it.id==memberId} + if (member) + state.members.remove(member) + } + + } +} + +def generateInitialEvent (member, childDevice) { + + // lets figure out if the member is currently "home" (At the place) + + try { // we are going to just ignore any errors + + log.debug "Generate Initial Event for New Device for Member = ${member.id}" + + def place = state.places.find{it.id==settings.place} + + if (place) { + + def memberLatitude = new Float (member.location.latitude) + def memberLongitude = new Float (member.location.longitude) + def placeLatitude = new Float (place.latitude) + def placeLongitude = new Float (place.longitude) + def placeRadius = new Float (place.radius) + + // log.debug "Member Location = ${memberLatitude}/${memberLongitude}" + // log.debug "Place Location = ${placeLatitude}/${placeLongitude}" + // log.debug "Place Radius = ${placeRadius}" + + def distanceAway = haversine(memberLatitude, memberLongitude, placeLatitude, placeLongitude)*1000 // in meters + + // log.debug "Distance Away = ${distanceAway}" + + boolean isPresent = (distanceAway <= placeRadius) + + // log.debug "External Id=${app.id}:${member.id}" + + // def childDevice2 = getChildDevice("${app.id}.${member.id}") + + // log.debug "Child Device = ${childDevice2}" + + childDevice?.generatePresenceEvent(isPresent) + + // log.debug "After generating presence event." + + } + + } + catch (e) { + // eat it + } + +} + +def initialize() { + // TODO: subscribe to attributes, devices, locations, etc. +} + +def haversine(lat1, lon1, lat2, lon2) { + def R = 6372.8 + // In kilometers + def dLat = Math.toRadians(lat2 - lat1) + def dLon = Math.toRadians(lon2 - lon1) + lat1 = Math.toRadians(lat1) + lat2 = Math.toRadians(lat2) + + def a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2) + def c = 2 * Math.asin(Math.sqrt(a)) + def d = R * c + return(d) +} + + +def placeEventHandler() { + + log.debug "In placeEventHandler method." + + // the POST to this end-point will look like: + // POST http://test.com/webhook?circleId=XXXX&placeId=XXXX&userId=XXXX&direction=arrive + + def circleId = params?.circleId + def placeId = params?.placeId + def userId = params?.userId + def direction = params?.direction + def timestamp = params?.timestamp + + log.debug "Life360 Event: Circle: ${circleId}, Place: ${placeId}, User: ${userId}, Direction: ${direction}" + + if (placeId == settings.place) { + + def presenceState = (direction=="in") + + def externalId = "${app.id}.${userId}" + + // find the appropriate child device based on my app id and the device network id + + def deviceWrapper = getChildDevice("${externalId}") + + // invoke the generatePresenceEvent method on the child device + + if (deviceWrapper) { + deviceWrapper.generatePresenceEvent(presenceState) + log.debug "Event raised on child device: ${externalId}" + } + else { + log.debug "Couldn't find child device associated with inbound Life360 event." + } + } + +} diff --git a/smartapps/smartthings/light-follows-me.src/light-follows-me.groovy b/smartapps/smartthings/light-follows-me.src/light-follows-me.groovy new file mode 100644 index 00000000000..ddc1a0af4fe --- /dev/null +++ b/smartapps/smartthings/light-follows-me.src/light-follows-me.groovy @@ -0,0 +1,74 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Light Follows Me + * + * Author: SmartThings + */ + +definition( + name: "Light Follows Me", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights on when motion is detected and then off again once the motion stops for a set period of time.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png" +) + +preferences { + section("Turn on when there's movement..."){ + input "motion1", "capability.motionSensor", title: "Where?" + } + section("And off when there's been no movement for..."){ + input "minutes1", "number", title: "Minutes?" + } + section("Turn on/off light(s)..."){ + input "switches", "capability.switch", multiple: true + } +} + +def installed() { + subscribe(motion1, "motion", motionHandler) +} + +def updated() { + unsubscribe() + subscribe(motion1, "motion", motionHandler) +} + +def motionHandler(evt) { + log.debug "$evt.name: $evt.value" + if (evt.value == "active") { + log.debug "turning on lights" + switches.on() + } else if (evt.value == "inactive") { + runIn(minutes1 * 60, scheduleCheck, [overwrite: false]) + } +} + +def scheduleCheck() { + log.debug "schedule check" + def motionState = motion1.currentState("motion") + if (motionState.value == "inactive") { + def elapsed = now() - motionState.rawDateCreated.time + def threshold = 1000 * 60 * minutes1 - 1000 + if (elapsed >= threshold) { + log.debug "Motion has stayed inactive long enough since last check ($elapsed ms): turning lights off" + switches.off() + } else { + log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): doing nothing" + } + } else { + log.debug "Motion is active, do nothing and wait for inactive" + } +} diff --git a/smartapps/smartthings/light-up-the-night.src/light-up-the-night.groovy b/smartapps/smartthings/light-up-the-night.src/light-up-the-night.groovy new file mode 100644 index 00000000000..4f3c2afeb22 --- /dev/null +++ b/smartapps/smartthings/light-up-the-night.src/light-up-the-night.groovy @@ -0,0 +1,56 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Light Up The Night + * + * Author: SmartThings + */ +definition( + name: "Light Up the Night", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights on when it gets dark and off when it becomes light again.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet-luminance.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet-luminance@2x.png" +) + +preferences { + section("Monitor the luminosity...") { + input "lightSensor", "capability.illuminanceMeasurement" + } + section("Turn on a light...") { + input "lights", "capability.switch", multiple: true + } +} + +def installed() { + subscribe(lightSensor, "illuminance", illuminanceHandler) +} + +def updated() { + unsubscribe() + subscribe(lightSensor, "illuminance", illuminanceHandler) +} + +// New aeon implementation +def illuminanceHandler(evt) { + def lastStatus = state.lastStatus + if (lastStatus != "on" && evt.integerValue < 30) { + lights.on() + state.lastStatus = "on" + } + else if (lastStatus != "off" && evt.integerValue > 50) { + lights.off() + state.lastStatus = "off" + } +} diff --git a/smartapps/smartthings/lights-off-when-closed.src/lights-off-when-closed.groovy b/smartapps/smartthings/lights-off-when-closed.src/lights-off-when-closed.groovy new file mode 100644 index 00000000000..2813e9b9ded --- /dev/null +++ b/smartapps/smartthings/lights-off-when-closed.src/lights-off-when-closed.groovy @@ -0,0 +1,49 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Lights Off, When Closed + * + * Author: SmartThings + */ +definition( + name: "Lights Off, When Closed", + namespace: "smartthings", + author: "SmartThings", + description: "Turn your lights off when an open/close sensor closes.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png" +) + +preferences { + section ("When the door closes...") { + input "contact1", "capability.contactSensor", title: "Where?" + } + section ("Turn off a light...") { + input "switch1", "capability.switch" + } +} + +def installed() +{ + subscribe(contact1, "contact.closed", contactClosedHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contact1, "contact.closed", contactClosedHandler) +} + +def contactClosedHandler(evt) { + switch1.off() +} diff --git a/smartapps/smartthings/lock-it-when-i-leave.src/lock-it-when-i-leave.groovy b/smartapps/smartthings/lock-it-when-i-leave.src/lock-it-when-i-leave.groovy new file mode 100644 index 00000000000..1378ece942b --- /dev/null +++ b/smartapps/smartthings/lock-it-when-i-leave.src/lock-it-when-i-leave.groovy @@ -0,0 +1,87 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Lock It When I Leave + * + * Author: SmartThings + * Date: 2013-02-11 + */ + +definition( + name: "Lock It When I Leave", + namespace: "smartthings", + author: "SmartThings", + description: "Locks a deadbolt or lever lock when a SmartSense Presence tag or smartphone leaves a location.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + oauth: true +) + +preferences { + section("When I leave...") { + input "presence1", "capability.presenceSensor", title: "Who?", multiple: true + } + section("Lock the lock...") { + input "lock1","capability.lock", multiple: true + input "unlock", "enum", title: "Unlock when presence is detected?", options: ["Yes","No"] + input("recipients", "contact", title: "Send notifications to") { + input "spam", "enum", title: "Send Me Notifications?", options: ["Yes", "No"] + } + } +} + +def installed() +{ + subscribe(presence1, "presence", presence) +} + +def updated() +{ + unsubscribe() + subscribe(presence1, "presence", presence) +} + +def presence(evt) +{ + if (evt.value == "present") { + if (unlock == "Yes") { + def anyLocked = lock1.count{it.currentLock == "unlocked"} != lock1.size() + if (anyLocked) { + sendMessage("Doors unlocked at arrival of $evt.linkText") + } + lock1.unlock() + } + } + else { + def nobodyHome = presence1.find{it.currentPresence == "present"} == null + if (nobodyHome) { + def anyUnlocked = lock1.count{it.currentLock == "locked"} != lock1.size() + if (anyUnlocked) { + sendMessage("Doors locked after everyone departed") + } + lock1.lock() + } + } +} + +def sendMessage(msg) { + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (spam == "Yes") { + sendPush msg + } + } +} diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy new file mode 100644 index 00000000000..123f8a91705 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -0,0 +1,881 @@ +/** + * Harmony (Connect) - https://developer.Harmony.com/documentation + * + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Author: SmartThings + * + * For complete set of capabilities, attributes, and commands see: + * + * https://graph.api.smartthings.com/ide/doc/capabilities + * + * ---------------------+-------------------+-----------------------------+------------------------------------ + * Device Type | Attribute Name | Commands | Attribute Values + * ---------------------+-------------------+-----------------------------+------------------------------------ + * switches | switch | on, off | on, off + * motionSensors | motion | | active, inactive + * contactSensors | contact | | open, closed + * presenceSensors | presence | | present, 'not present' + * temperatureSensors | temperature | | + * accelerationSensors | acceleration | | active, inactive + * waterSensors | water | | wet, dry + * lightSensors | illuminance | | + * humiditySensors | humidity | | + * alarms | alarm | strobe, siren, both, off | strobe, siren, both, off + * locks | lock | lock, unlock | locked, unlocked + * ---------------------+-------------------+-----------------------------+------------------------------------ + */ + +definition( + name: "Logitech Harmony (Connect)", + namespace: "smartthings", + author: "Juan Pablo Risso", + description: "Allows you to integrate your Logitech Harmony account with SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", + oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"] +){ + appSetting "clientId" + appSetting "clientSecret" + appSetting "callbackUrl" +} + +preferences(oauthPage: "deviceAuthorization") { + page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") + page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) { + section("Allow Logitech Harmony to control these things...") { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false + input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false + input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false + input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false + input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false + input "waterSensors", "capability.waterSensor", title: "Which Water Sensors?", multiple: true, required: false + input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false + input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false + input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + } + } +} + +mappings { + path("/devices") { action: [ GET: "listDevices"] } + path("/devices/:id") { action: [ GET: "getDevice", PUT: "updateDevice"] } + path("/subscriptions") { action: [ GET: "listSubscriptions", POST: "addSubscription"] } + path("/subscriptions/:id") { action: [ DELETE: "removeSubscription"] } + path("/phrases") { action: [ GET: "listPhrases"] } + path("/phrases/:id") { action: [ PUT: "executePhrase"] } + path("/hubs") { action: [ GET: "listHubs" ] } + path("/hubs/:id") { action: [ GET: "getHub" ] } + path("/activityCallback/:dni") { action: [ POST: "activityCallback" ] } + path("/harmony") { action: [ GET: "getHarmony", POST: "harmony" ] } + path("/harmony/:mac") { action: [ DELETE: "deleteHarmony" ] } + path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } + path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } + path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } + path("/oauth/callback") { action: [ GET: "callback" ] } + path("/oauth/initialize") { action: [ GET: "init"] } +} + +def getServerUrl() { return "https://graph.api.smartthings.com" } + +def authPage() { + def description = null + if (!state.HarmonyAccessToken) { + if (!state.accessToken) { + log.debug "About to create access token" + createAccessToken() + } + description = "Click to enter Harmony Credentials" + def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" + return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { + section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } + } + } else { + //device discovery request every 5 //25 seconds + int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int + state.deviceRefreshCount = deviceRefreshCount + 1 + def refreshInterval = 3 + + def huboptions = state.HarmonyHubs ?: [] + def actoptions = state.HarmonyActivities ?: [] + + def numFoundHub = huboptions.size() ?: 0 + def numFoundAct = actoptions.size() ?: 0 + if((deviceRefreshCount % 5) == 0) { + discoverDevices() + } + return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { + section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { + input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions + } + if (numFoundHub > 0 && numFoundAct > 0 && false) + section("You can also add activities as virtual switches for other convenient integrations") { + input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions + } + if (state.resethub) + section("Connection to the hub timed out. Please restart the hub and try again.") {} + } + } +} + +def callback() { + def redirectUrl = null + if (params.authQueryString) { + redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) + log.debug "redirectUrl: ${redirectUrl}" + } else { + log.warn "No authQueryString" + } + + if (state.HarmonyAccessToken) { + log.debug "Access token already exists" + discovery() + success() + } else { + def code = params.code + if (code) { + if (code.size() > 6) { + // Harmony code + log.debug "Exchanging code for access token" + receiveToken(redirectUrl) + } else { + // Initiate the Harmony OAuth flow. + init() + } + } else { + log.debug "This code should be unreachable" + success() + } + } +} + +def init() { + log.debug "Requesting Code" + def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ] + redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") +} + +def receiveToken(redirectUrl = null) { + log.debug "receiveToken" + def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ] + def params = [ + uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", + ] + try { + httpPost(params) { response -> + state.HarmonyAccessToken = response.data.access_token + } + } catch (java.util.concurrent.TimeoutException e) { + fail(e) + log.warn "Connection timed out, please try again later." + } + discovery() + if (state.HarmonyAccessToken) { + success() + } else { + fail("") + } +} + +def success() { + def message = """ +

Your Harmony Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

+ """ + connectionStatus(message) +} + +def fail(msg) { + def message = """ +

The connection could not be established!

+

$msg

+

Click 'Done' to return to the menu.

+ """ + connectionStatus(message) +} + +def receivedToken() { + def message = """ +

Your Harmony Account is already connected to SmartThings!

+

Click 'Done' to finish setup.

+ """ + connectionStatus(message) +} + +def connectionStatus(message, redirectUrl = null) { + def redirectHtml = "" + if (redirectUrl) { + redirectHtml = """ + + """ + } + + def html = """ + + + + + SmartThings Connection + + ${redirectHtml} + + +
+ Harmony icon + connected device icon + SmartThings logo + ${message} +
+ + + """ + render contentType: 'text/html', data: html +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +def buildRedirectUrl(page) { + return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" +} + +def installed() { + enableCallback() + if (!state.accessToken) { + log.debug "About to create access token" + createAccessToken() + } else { + initialize() + } +} + +def updated() { + unsubscribe() + unschedule() + enableCallback() + if (!state.accessToken) { + log.debug "About to create access token" + createAccessToken() + } else { + initialize() + } +} + +def uninstalled() { + if (state.HarmonyAccessToken) { + try { + state.HarmonyAccessToken = "" + log.debug "Success disconnecting Harmony from SmartThings" + } catch (groovyx.net.http.HttpResponseException e) { + log.error "Error disconnecting Harmony from SmartThings: ${e.statusCode}" + } + } +} + +def initialize() { + state.aux = 0 + if (selectedhubs || selectedactivities) { + addDevice() + runEvery5Minutes("discovery") + } +} + +def getHarmonydevices() { + state.Harmonydevices ?: [] +} + +Map discoverDevices() { + log.trace "Discovering devices..." + discovery() + if (getHarmonydevices() != []) { + def devices = state.Harmonydevices.hubs + log.trace devices.toString() + def activities = [:] + def hubs = [:] + devices.each { + def hubkey = it.key + def hubname = getHubName(it.key) + def hubvalue = "${hubname}" + hubs["harmony-${hubkey}"] = hubvalue + it.value.response.data.activities.each { + def value = "${it.value.name}" + def key = "harmony-${hubkey}-${it.key}" + activities["${key}"] = value + } + } + state.HarmonyHubs = hubs + state.HarmonyActivities = activities + } +} + +//CHILD DEVICE METHODS +def discovery() { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + if (response.status == 200) { + log.debug "valid Token" + state.Harmonydevices = response.data + state.resethub = false + getActivityList() + poll() + } else { + log.debug "Error: $response.status" + } + } + } catch (groovyx.net.http.HttpResponseException e) { + if (e.statusCode == 401) { // token is expired + state.remove("HarmonyAccessToken") + log.warn "Harmony Access token has expired" + } + } catch (java.net.SocketTimeoutException e) { + log.warn "Connection to the hub timed out. Please restart the hub and try again." + state.resethub = true + } + return null +} + +def addDevice() { + log.trace "Adding Hubs" + selectedhubs.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newAction = state.HarmonyHubs.find { it.key == dni } + d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"]) + log.trace "created ${d.displayName} with id $dni" + poll() + } else { + log.trace "found ${d.displayName} with id $dni already exists" + } + } + log.trace "Adding Activities" + selectedactivities.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newAction = state.HarmonyActivities.find { it.key == dni } + d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) + log.trace "created ${d.displayName} with id $dni" + poll() + } else { + log.trace "found ${d.displayName} with id $dni already exists" + } + } +} + +def activity(dni,mode) { + def Params = [auth: state.HarmonyAccessToken] + def msg = "Command failed" + def url = '' + if (dni == "all") { + url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}" + } else { + def aux = dni.split('-') + def hubId = aux[1] + if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ + url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}" + } else { + def activityId = aux[2] + url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}" + } + } + try { + httpPostJson(uri: url) { response -> + if (response.data.code == 200 || dni == "all") { + msg = "Command sent succesfully" + state.aux = 0 + } else { + msg = "Command failed. Error: $response.data.code" + } + } + } catch (groovyx.net.http.HttpResponseException ex) { + log.error ex + if (state.aux == 0) { + state.aux = 1 + activity(dni,mode) + } else { + msg = ex + state.aux = 0 + } + } + runIn(10, "poll", [overwrite: true]) + return msg +} + +def poll() { + // GET THE LIST OF ACTIVITIES + if (state.HarmonyAccessToken) { + getActivityList() + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + def map = [:] + response.data.hubs.each { + if (it.value.message == "OK") { + map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}" + def hub = getChildDevice("harmony-${it.key}") + if (hub) { + if (it.value.response.data.currentAvActivity == "-1") { + hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false) + } else { + def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) + hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false) + } + } + } else { + log.trace it.value.message + } + } + def activities = getChildDevices() + def activitynotrunning = true + activities.each { activity -> + def act = activity.deviceNetworkId.split('-') + if (act.size() > 2) { + def aux = map.find { it.key == act[1] } + if (aux) { + def aux2 = aux.value.split(',') + def childDevice = getChildDevice(activity.deviceNetworkId) + if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) { + childDevice?.sendEvent(name: "switch", value: "on") + if (aux2[1] == "1") + runIn(5, "poll", [overwrite: true]) + } else { + childDevice?.sendEvent(name: "switch", value: "off") + if (aux2[1] == "3") + runIn(5, "poll", [overwrite: true]) + } + } + } + } + return "Poll completed $map - $state.hubs" + } + } catch (groovyx.net.http.HttpResponseException e) { + if (e.statusCode == 401) { // token is expired + state.remove("HarmonyAccessToken") + return "Harmony Access token has expired" + } + } + } +} + + +def getActivityList() { + // GET ACTIVITY'S NAME + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + response.data.hubs.each { + def hub = getChildDevice("harmony-${it.key}") + if (hub) { + def hubname = getHubName("${it.key}") + def activities = [] + def aux = it.value.response.data.activities.size() + if (aux >= 1) { + activities = it.value.response.data.activities.collect { + [id: it.key, name: it.value['name'], type: it.value['type']] + } + activities += [id: "off", name: "Activity OFF", type: "0"] + log.trace activities + } + hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) + } + } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.trace e + } catch (java.net.SocketTimeoutException e) { + log.trace e + } + } + return activity +} + +def getActivityName(activity,hubId) { + // GET ACTIVITY'S NAME + def actname = activity + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + actname = response.data.data.activities[activity].name + } + } catch (groovyx.net.http.HttpResponseException e) { + log.trace e + } + } + return actname +} + +def getActivityId(activity,hubId) { + // GET ACTIVITY'S NAME + def actid = activity + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + response.data.data.activities.each { + if (it.value.name == activity) + actid = it.key + } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.trace e + } + } + return actid +} + +def getHubName(hubId) { + // GET HUB'S NAME + def hubname = hubId + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + hubname = response.data.data.name + } + } catch (groovyx.net.http.HttpResponseException e) { + log.trace e + } + } + return hubname +} + +def sendNotification(msg) { + sendNotification(msg) +} + +def hookEventHandler() { + // log.debug "In hookEventHandler method." + log.debug "request = ${request}" + + def json = request.JSON + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html +} + +def listDevices() { + log.debug "getDevices, params: ${params}" + allDevices.collect { + deviceItem(it) + } +} + +def getDevice() { + log.debug "getDevice, params: ${params}" + def device = allDevices.find { it.id == params.id } + if (!device) { + render status: 404, data: '{"msg": "Device not found"}' + } else { + deviceItem(device) + } +} + +def updateDevice() { + def data = request.JSON + def command = data.command + def arguments = data.arguments + + log.debug "updateDevice, params: ${params}, request: ${data}" + if (!command) { + render status: 400, data: '{"msg": "command is required"}' + } else { + def device = allDevices.find { it.id == params.id } + if (device) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + render status: 204, data: "{}" + } else { + render status: 404, data: '{"msg": "Device not found"}' + } + } +} + +def listSubscriptions() { + log.debug "listSubscriptions()" + app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect { + def deviceInfo = state[it.device.id] + def response = [ + id: it.id, + deviceId: it.device.id, + attributeName: it.data, + handler: it.handler + ] + if (!state.harmonyHubs) { + response.callbackUrl = deviceInfo?.callbackUrl + } + response + } ?: [] +} + +def addSubscription() { + def data = request.JSON + def attribute = data.attributeName + def callbackUrl = data.callbackUrl + + log.debug "addSubscription, params: ${params}, request: ${data}" + if (!attribute) { + render status: 400, data: '{"msg": "attributeName is required"}' + } else { + def device = allDevices.find { it.id == data.deviceId } + if (device) { + if (!state.harmonyHubs) { + log.debug "Adding callbackUrl: $callbackUrl" + state[device.id] = [callbackUrl: callbackUrl] + } + log.debug "Adding subscription" + def subscription = subscribe(device, attribute, deviceHandler) + if (!subscription || !subscription.eventSubscription) { + subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' } + } + + def response = [ + id: subscription.id, + deviceId: subscription.device.id, + attributeName: subscription.data, + handler: subscription.handler + ] + if (!state.harmonyHubs) { + response.callbackUrl = callbackUrl + } + response + } else { + render status: 400, data: '{"msg": "Device not found"}' + } + } +} + +def removeSubscription() { + def subscription = app.subscriptions?.find { it.id == params.id } + def device = subscription?.device + + log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}" + if (device) { + log.debug "Removing subscription for device: ${device.id}" + state.remove(device.id) + unsubscribe(device) + } + render status: 204, data: "{}" +} + +def listPhrases() { + location.helloHome.getPhrases()?.collect {[ + id: it.id, + label: it.label + ]} +} + +def executePhrase() { + log.debug "executedPhrase, params: ${params}" + location.helloHome.execute(params.id) + render status: 204, data: "{}" +} + +def deviceHandler(evt) { + def deviceInfo = state[evt.deviceId] + if (state.harmonyHubs) { + state.harmonyHubs.each { harmonyHub -> + sendToHarmony(evt, harmonyHub.callbackUrl) + } + } else if (deviceInfo) { + if (deviceInfo.callbackUrl) { + sendToHarmony(evt, deviceInfo.callbackUrl) + } else { + log.warn "No callbackUrl set for device: ${evt.deviceId}" + } + } else { + log.warn "No subscribed device found for device: ${evt.deviceId}" + } +} + +def sendToHarmony(evt, String callbackUrl) { + def callback = new URI(callbackUrl) + def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host + def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path + sendHubCommand(new physicalgraph.device.HubAction( + method: "POST", + path: path, + headers: [ + "Host": host, + "Content-Type": "application/json" + ], + body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] + )) +} + +def listHubs() { + location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) } +} + +def getHub() { + def hub = location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.find { it.id == params.id } + if (!hub) { + render status: 404, data: '{"msg": "Hub not found"}' + } else { + hubItem(hub) + } +} + +def activityCallback() { + def data = request.JSON + def device = getChildDevice(params.dni) + if (device) { + if (data.errorCode == "200") { + device.setCurrentActivity(data.currentActivityId) + } else { + log.warn "Activity callback error: ${data}" + } + } else { + log.warn "Activity callback sent to non-existant dni: ${params.dni}" + } + render status: 200, data: '{"msg": "Successfully received callbackUrl"}' +} + +def getHarmony() { + state.harmonyHubs ?: [] +} + +def harmony() { + def data = request.JSON + if (data.mac && data.callbackUrl && data.name) { + if (!state.harmonyHubs) { state.harmonyHubs = [] } + def harmonyHub = state.harmonyHubs.find { it.mac == data.mac } + if (harmonyHub) { + harmonyHub.mac = data.mac + harmonyHub.callbackUrl = data.callbackUrl + harmonyHub.name = data.name + } else { + state.harmonyHubs << [mac: data.mac, callbackUrl: data.callbackUrl, name: data.name] + } + render status: 200, data: '{"msg": "Successfully received Harmony data"}' + } else { + if (!data.mac) { + render status: 400, data: '{"msg": "mac is required"}' + } else if (!data.callbackUrl) { + render status: 400, data: '{"msg": "callbackUrl is required"}' + } else if (!data.name) { + render status: 400, data: '{"msg": "name is required"}' + } + } +} + +def deleteHarmony() { + log.debug "Trying to delete Harmony hub with mac: ${params.mac}" + def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac } + if (harmonyHub) { + log.debug "Deleting Harmony hub with mac: ${params.mac}" + state.harmonyHubs.remove(harmonyHub) + } else { + log.debug "Couldn't find Harmony hub with mac: ${params.mac}" + } + render status: 204, data: "{}" +} + +private getAllDevices() { + ([] + switches + motionSensors + contactSensors + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id } +} + +private deviceItem(device) { + [ + id: device.id, + label: device.displayName, + currentStates: device.currentStates, + capabilities: device.capabilities?.collect {[ + name: it.name + ]}, + attributes: device.supportedAttributes?.collect {[ + name: it.name, + dataType: it.dataType, + values: it.values + ]}, + commands: device.supportedCommands?.collect {[ + name: it.name, + arguments: it.arguments + ]}, + type: [ + name: device.typeName, + author: device.typeAuthor + ] + ] +} + +private hubItem(hub) { + [ + id: hub.id, + name: hub.name, + ip: hub.localIP, + port: hub.localSrvPortTCP + ] +} diff --git a/smartapps/smartthings/mail-arrived.src/mail-arrived.groovy b/smartapps/smartthings/mail-arrived.src/mail-arrived.groovy new file mode 100644 index 00000000000..333a8fa99c5 --- /dev/null +++ b/smartapps/smartthings/mail-arrived.src/mail-arrived.groovy @@ -0,0 +1,77 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Mail Arrived + * + * Author: SmartThings + */ +definition( + name: "Mail Arrived", + namespace: "smartthings", + author: "SmartThings", + description: "Send a text when mail arrives in your mailbox using a SmartSense Multi on your mailbox door. Note: battery life may be impacted in cold climates.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/mail_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/mail_contact@2x.png" +) + +preferences { + section("When mail arrives...") { + input "accelerationSensor", "capability.accelerationSensor", title: "Where?" + } + section("Notify me...") { + input("recipients", "contact", title: "Send notifications to") { + input "pushNotification", "bool", title: "Push notification", required: false, defaultValue: "true" + input "phone1", "phone", title: "Phone number", required: false + } + } +} + +def installed() { + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def updated() { + unsubscribe() + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def accelerationActiveHandler(evt) { + log.trace "$evt.value: $evt, $settings" + + // Don't send a continuous stream of notifications + def deltaSeconds = 5 + def timeAgo = new Date(now() - (1000 * deltaSeconds)) + def recentEvents = accelerationSensor.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds" + def alreadySentNotifications = recentEvents.count { it.value && it.value == "active" } > 1 + + if (alreadySentNotifications) { + log.debug "Notifications already sent within the last $deltaSeconds seconds (phone1: $phone1, pushNotification: $pushNotification)" + } + else { + if (location.contactBookEnabled) { + log.debug "$accelerationSensor has moved, notifying ${recipients?.size()}" + sendNotificationToContacts("Mail has arrived!", recipients) + } + else { + if (phone1 != null && phone1 != "") { + log.debug "$accelerationSensor has moved, texting $phone1" + sendSms(phone1, "Mail has arrived!") + } + if (pushNotification) { + log.debug "$accelerationSensor has moved, sending push" + sendPush("Mail has arrived!") + } + } + } +} diff --git a/smartapps/smartthings/make-it-so.src/make-it-so.groovy b/smartapps/smartthings/make-it-so.src/make-it-so.groovy new file mode 100644 index 00000000000..718d69ef744 --- /dev/null +++ b/smartapps/smartthings/make-it-so.src/make-it-so.groovy @@ -0,0 +1,137 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Make it So + * + * Author: SmartThings + * Date: 2013-03-06 + */ +definition( + name: "Make It So", + namespace: "smartthings", + author: "SmartThings", + description: "Saves the states of a specified set switches and thermostat setpoints and restores them at each mode change. To use 1) Set the mode, 2) Change switches and setpoint to where you want them for that mode, and 3) Install or update the app. Changing to that mode or touching the app will set the devices to the saved state.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_thermo-switch.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_thermo-switch@2x.png" +) + +preferences { + section("Switches") { + input "switches", "capability.switch", multiple: true, required: false + } + section("Thermostats") { + input "thermostats", "capability.thermostat", multiple: true, required: false + } + section("Locks") { + input "locks", "capability.lock", multiple: true, required: false + } +} + +def installed() { + subscribe(location, changedLocationMode) + subscribe(app, appTouch) + saveState() +} + +def updated() { + unsubscribe() + subscribe(location, changedLocationMode) + subscribe(app, appTouch) + saveState() +} + +def appTouch(evt) +{ + restoreState(currentMode) +} + +def changedLocationMode(evt) +{ + restoreState(evt.value) +} + +private restoreState(mode) +{ + log.info "restoring state for mode '$mode'" + def map = state[mode] ?: [:] + switches?.each { + def value = map[it.id] + if (value?.switch == "on") { + def level = value.level + if (level) { + log.debug "setting $it.label level to $level" + it.setLevel(level) + } + else { + log.debug "turning $it.label on" + it.on() + } + } + else if (value?.switch == "off") { + log.debug "turning $it.label off" + it.off() + } + } + + thermostats?.each { + def value = map[it.id] + if (value?.coolingSetpoint) { + log.debug "coolingSetpoint = $value.coolingSetpoint" + it.setCoolingSetpoint(value.coolingSetpoint) + } + if (value?.heatingSetpoint) { + log.debug "heatingSetpoint = $value.heatingSetpoint" + it.setHeatingSetpoint(value.heatingSetpoint) + } + } + + locks?.each { + def value = map[it.id] + if (value) { + if (value?.locked) { + it.lock() + } + else { + it.unlock() + } + } + } +} + + +private saveState() +{ + def mode = currentMode + def map = state[mode] ?: [:] + + switches?.each { + map[it.id] = [switch: it.currentSwitch, level: it.currentLevel] + } + + thermostats?.each { + map[it.id] = [coolingSetpoint: it.currentCoolingSetpoint, heatingSetpoint: it.currentHeatingSetpoint] + } + + locks?.each { + map[it.id] = [locked: it.currentLock == "locked"] + } + + state[mode] = map + log.debug "saved state for mode ${mode}: ${state[mode]}" + log.debug "state: $state" +} + +private getCurrentMode() +{ + location.mode ?: "_none_" +} diff --git a/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy b/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy new file mode 100644 index 00000000000..7c0f6935c17 --- /dev/null +++ b/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy @@ -0,0 +1,124 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Medicine Reminder + * + * Author: SmartThings + */ + +definition( + name: "Medicine Reminder", + namespace: "smartthings", + author: "SmartThings", + description: "Set up a reminder so that if you forget to take your medicine (determined by whether a cabinet or drawer has been opened) by specified time you get a notification or text message.", + category: "Health & Wellness", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact@2x.png" +) + +preferences { + section("Choose your medicine cabinet..."){ + input "cabinet1", "capability.contactSensor", title: "Where?" + } + section("Take my medicine at..."){ + input "time1", "time", title: "Time 1" + input "time2", "time", title: "Time 2", required: false + input "time3", "time", title: "Time 3", required: false + input "time4", "time", title: "Time 4", required: false + } + section("I forget send me a notification and/or text message..."){ + input("recipients", "contact", title: "Send notifications to") { + input "sendPush", "enum", title: "Push Notification", required: false, options: ["Yes", "No"] + input "phone1", "phone", title: "Phone Number", required: false + } + } + section("Time window (optional, defaults to plus or minus 15 minutes") { + input "timeWindow", "decimal", title: "Minutes", required: false + } +} + +def installed() +{ + initialize() +} + +def updated() +{ + unschedule() + initialize() +} + +def initialize() { + def window = timeWindowMsec + [time1, time2, time3, time4].eachWithIndex {time, index -> + if (time != null) { + def endTime = new Date(timeToday(time, location?.timeZone).time + window) + log.debug "Scheduling check at $endTime" + //runDaily(endTime, "scheduleCheck${index}") + switch (index) { + case 0: + schedule(endTime, scheduleCheck0) + break + case 1: + schedule(endTime, scheduleCheck1) + break + case 2: + schedule(endTime, scheduleCheck2) + break + case 3: + schedule(endTime, scheduleCheck3) + break + } + } + } +} + +def scheduleCheck0() { scheduleCheck() } +def scheduleCheck1() { scheduleCheck() } +def scheduleCheck2() { scheduleCheck() } +def scheduleCheck3() { scheduleCheck() } + +def scheduleCheck() +{ + log.debug "scheduleCheck" + def t0 = new Date(now() - (2 * timeWindowMsec)) + def t1 = new Date() + def cabinetOpened = cabinet1.eventsBetween(t0, t1).find{it.name == "contact" && it.value == "open"} + log.trace "Looking for events between $t0 and $t1: $cabinetOpened" + + if (cabinetOpened) { + log.trace "Medicine cabinet was opened since $midnight, no notification required" + } else { + log.trace "Medicine cabinet was not opened since $midnight, sending notification" + sendMessage() + } +} + +private sendMessage() { + def msg = "Please remember to take your medicine" + log.info msg + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (phone1) { + sendSms(phone1, msg) + } + if (sendPush == "Yes") { + sendPush(msg) + } + } +} + +def getTimeWindowMsec() { + (timeWindow ?: 15) * 60000 as Long +} diff --git a/smartapps/smartthings/mini-hue-controller.src/mini-hue-controller.groovy b/smartapps/smartthings/mini-hue-controller.src/mini-hue-controller.groovy new file mode 100644 index 00000000000..051022be733 --- /dev/null +++ b/smartapps/smartthings/mini-hue-controller.src/mini-hue-controller.groovy @@ -0,0 +1,160 @@ +/** + * Mini Hue Controller + * + * Copyright 2014 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. + * + */ +definition( + name: "Mini Hue Controller", + namespace: "smartthings", + author: "SmartThings", + description: "Control one or more Hue bulbs using an Aeon MiniMote.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + + +preferences { + section("Control these lights") { + input "bulbs", "capability.colorControl", title: "Hue light bulbs", multiple: true + } + section("Using this controller") { + input "controller", "capability.button", title: "Aeon minimote" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + state.colorIndex = -1 + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe controller, "button", buttonHandler +} + +def buttonHandler(evt) { + switch(evt.jsonData?.buttonNumber) { + case 2: + if (evt.value == "held") { + bulbs.setLevel(100) + } + else { + levelUp() + } + break + + case 3: + if (evt.value == "held") { + def color = [name:"Soft White", hue: 23, saturation: 56] + bulbs.setColor(hue: color.hue, saturation: color.saturation) + } + else { + changeColor() + } + break + + case 4: + if (evt.value == "held") { + bulbs.setLevel(10) + } + else { + levelDown() + } + break + + default: + toggleState() + break + } +} + +private toggleState() { + if (currentSwitchState == "on") { + log.debug "off" + bulbs.off() + } + else { + log.debug "on" + bulbs.on() + } +} + +private levelUp() { + def level = Math.min(currentSwitchLevel + 10, 100) + log.debug "level = $level" + bulbs.setLevel(level) +} + +private levelDown() { + def level = Math.max(currentSwitchLevel - 10, 10) + log.debug "level = $level" + bulbs.setLevel(level) +} + +private changeColor() { + + final colors = [ + [name:"Soft White", hue: 23, saturation: 56], + [name:"Daylight", hue: 53, saturation: 91], + [name:"White", hue: 52, saturation: 19], + [name:"Warm White", hue: 20, saturation: 80], + [name:"Blue", hue: 70, saturation: 100], + [name:"Green", hue: 39, saturation: 100], + [name:"Yellow", hue: 25, saturation: 100], + [name:"Orange", hue: 10, saturation: 100], + [name:"Purple", hue: 75, saturation: 100], + [name:"Pink", hue: 83, saturation: 100], + [name:"Red", hue: 100, saturation: 100] + ] + + final maxIndex = colors.size() - 1 + + if (state.colorIndex < maxIndex) { + state.colorIndex = state.colorIndex + 1 + } + else { + state.colorIndex = 0 + } + + def color = colors[state.colorIndex] + bulbs.setColor(hue: color.hue, saturation: color.saturation) +} + +private getCurrentSwitchState() { + def on = 0 + def off = 0 + bulbs.each { + if (it.currentValue("switch") == "on") { + on++ + } + else { + off++ + } + } + on > off ? "on" : "off" +} + +private getCurrentSwitchLevel() { + def level = 0 + bulbs.each { + level = Math.max(it.currentValue("level")?.toInteger() ?: 0, level) + } + level.toInteger() +} diff --git a/smartapps/smartthings/mood-cube.src/mood-cube.groovy b/smartapps/smartthings/mood-cube.src/mood-cube.groovy new file mode 100644 index 00000000000..41e87c9129f --- /dev/null +++ b/smartapps/smartthings/mood-cube.src/mood-cube.groovy @@ -0,0 +1,344 @@ +/** + * Mood Cube + * + * Copyright 2014 SmartThings, Inc. + * + * 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: "Mood Cube", + namespace: "smartthings", + author: "SmartThings", + description: "Set your lighting by rotating a cube containing a SmartSense Multi", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png" +) + +/********** + * Setup * + **********/ +preferences { + page(name: "mainPage", title: "", nextPage: "scenesPage", uninstall: true) { + section("Use the orientation of this cube") { + input "cube", "capability.threeAxis", required: false, title: "SmartSense Multi sensor" + } + section("To control these lights") { + input "lights", "capability.switch", multiple: true, required: false, title: "Lights, switches & dimmers" + } + section([title: " ", mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } + page(name: "scenesPage", title: "Scenes", install: true, uninstall: true) + page(name: "scenePage", title: "Scene", install: false, uninstall: false, previousPage: "scenesPage") + page(name: "devicePage", install: false, uninstall: false, previousPage: "scenePage") + page(name: "saveStatesPage", install: false, uninstall: false, previousPage: "scenePage") +} + + +def scenesPage() { + log.debug "scenesPage()" + def sceneId = getOrientation() + dynamicPage(name:"scenesPage") { + section { + for (num in 1..6) { + href "scenePage", title: "${num}. ${sceneName(num)}${sceneId==num ? ' (current)' : ''}", params: [sceneId:num], description: "", state: sceneIsDefined(num) ? "complete" : "incomplete" + } + } + section { + href "scenesPage", title: "Refresh", description: "" + } + } +} + +def scenePage(params=[:]) { + log.debug "scenePage($params)" + def currentSceneId = getOrientation() + def sceneId = params.sceneId as Integer ?: state.lastDisplayedSceneId + state.lastDisplayedSceneId = sceneId + dynamicPage(name:"scenePage", title: "${sceneId}. ${sceneName(sceneId)}") { + section { + input "sceneName${sceneId}", "text", title: "Scene Name", required: false + } + + section { + href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete" + } + + if (sceneId == currentSceneId) { + section { + href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: "" + } + } + + } +} + +def devicePage(params) { + log.debug "devicePage($params)" + + getDeviceCapabilities() + + def sceneId = params.sceneId as Integer ?: state.lastDisplayedSceneId + + dynamicPage(name:"devicePage", title: "${sceneId}. ${sceneName(sceneId)} Device States") { + section("Lights") { + lights.each {light -> + input "onoff_${sceneId}_${light.id}", "boolean", title: light.displayName + } + } + + section("Dimmers") { + lights.each {light -> + if (state.lightCapabilities[light.id] in ["level", "color"]) { + input "level_${sceneId}_${light.id}", "enum", title: light.displayName, options: levels, description: "", required: false + } + } + } + + section("Colors (hue/saturation)") { + lights.each {light -> + if (state.lightCapabilities[light.id] == "color") { + input "color_${sceneId}_${light.id}", "text", title: light.displayName, description: "", required: false + } + } + } + } +} + +def saveStatesPage(params) { + saveStates(params) + devicePage(params) +} + + +/************************* + * Installation & update * + *************************/ +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe cube, "threeAxis", positionHandler +} + + +/****************** + * Event handlers * + ******************/ +def positionHandler(evt) { + + def sceneId = getOrientation(evt.xyzValue) + log.trace "orientation: $sceneId" + + if (sceneId != state.lastActiveSceneId) { + restoreStates(sceneId) + } + else { + log.trace "No status change" + } + state.lastActiveSceneId = sceneId +} + + +/****************** + * Helper methods * + ******************/ +private Boolean sceneIsDefined(sceneId) { + def tgt = "onoff_${sceneId}".toString() + settings.find{it.key.startsWith(tgt)} != null +} + +private updateSetting(name, value) { + app.updateSetting(name, value) + settings[name] = value +} + +private closestLevel(level) { + level ? "${Math.round(level/5) * 5}%" : "0%" +} + +private saveStates(params) { + log.trace "saveStates($params)" + def sceneId = params.sceneId as Integer + getDeviceCapabilities() + + lights.each {light -> + def type = state.lightCapabilities[light.id] + + updateSetting("onoff_${sceneId}_${light.id}", light.currentValue("switch") == "on") + + if (type == "level") { + updateSetting("level_${sceneId}_${light.id}", closestLevel(light.currentValue('level'))) + } + else if (type == "color") { + updateSetting("level_${sceneId}_${light.id}", closestLevel(light.currentValue('level'))) + updateSetting("color_${sceneId}_${light.id}", "${light.currentValue("hue")}/${light.currentValue("saturation")}") + } + } +} + + +private restoreStates(sceneId) { + log.trace "restoreStates($sceneId)" + getDeviceCapabilities() + + lights.each {light -> + def type = state.lightCapabilities[light.id] + + def isOn = settings."onoff_${sceneId}_${light.id}" == "true" ? true : false + log.debug "${light.displayName} is '$isOn'" + if (isOn) { + light.on() + } + else { + light.off() + } + + if (type != "switch") { + def level = switchLevel(sceneId, light) + + if (type == "level") { + log.debug "${light.displayName} level is '$level'" + if (level != null) { + light.setLevel(value) + } + } + else if (type == "color") { + def segs = settings."color_${sceneId}_${light.id}"?.split("/") + if (segs?.size() == 2) { + def hue = segs[0].toInteger() + def saturation = segs[1].toInteger() + log.debug "${light.displayName} color is level: $level, hue: $hue, sat: $saturation" + if (level != null) { + light.setColor(level: level, hue: hue, saturation: saturation) + } + else { + light.setColor(hue: hue, saturation: saturation) + } + } + else { + log.debug "${light.displayName} level is '$level'" + if (level != null) { + light.setLevel(level) + } + } + } + else { + log.error "Unknown type '$type'" + } + } + + + } +} + +private switchLevel(sceneId, light) { + def percent = settings."level_${sceneId}_${light.id}" + if (percent) { + percent[0..-2].toInteger() + } + else { + null + } +} + +private getDeviceCapabilities() { + def caps = [:] + lights.each { + if (it.hasCapability("Color Control")) { + caps[it.id] = "color" + } + else if (it.hasCapability("Switch Level")) { + caps[it.id] = "level" + } + else { + caps[it.id] = "switch" + } + } + state.lightCapabilities = caps +} + +private getLevels() { + def levels = [] + for (int i = 0; i <= 100; i += 5) { + levels << "$i%" + } + levels +} + +private getOrientation(xyz=null) { + final threshold = 250 + + def value = xyz ?: cube.currentValue("threeAxis") + + def x = Math.abs(value.x) > threshold ? (value.x > 0 ? 1 : -1) : 0 + def y = Math.abs(value.y) > threshold ? (value.y > 0 ? 1 : -1) : 0 + def z = Math.abs(value.z) > threshold ? (value.z > 0 ? 1 : -1) : 0 + + def orientation = 0 + if (z > 0) { + if (x == 0 && y == 0) { + orientation = 1 + } + } + else if (z < 0) { + if (x == 0 && y == 0) { + orientation = 2 + } + } + else { + if (x > 0) { + if (y == 0) { + orientation = 3 + } + } + else if (x < 0) { + if (y == 0) { + orientation = 4 + } + } + else { + if (y > 0) { + orientation = 5 + } + else if (y < 0) { + orientation = 6 + } + } + } + + orientation +} + +private sceneName(num) { + final names = ["UNDEFINED","One","Two","Three","Four","Five","Six"] + settings."sceneName${num}" ?: "Scene ${names[num]}" +} + + + diff --git a/smartapps/smartthings/nfc-tag-toggle.src/nfc-tag-toggle.groovy b/smartapps/smartthings/nfc-tag-toggle.src/nfc-tag-toggle.groovy new file mode 100644 index 00000000000..497be4134d1 --- /dev/null +++ b/smartapps/smartthings/nfc-tag-toggle.src/nfc-tag-toggle.groovy @@ -0,0 +1,143 @@ +/** + * NFC Tag Toggle + * + * Copyright 2014 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. + * + */ + +definition( + name: "NFC Tag Toggle", + namespace: "smartthings", + author: "SmartThings", + description: "Allows toggling of a switch, lock, or garage door based on an NFC Tag touch event", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/nfc-tag-executor.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/nfc-tag-executor@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Developers/nfc-tag-executor@2x.png") + + +preferences { + page(name: "pageOne", title: "Device selection", uninstall: true, nextPage: "pageTwo") { + section("Select an NFC tag") { + input "tag", "capability.touchSensor", title: "NFC Tag" + } + section("Select devices to control") { + input "switch1", "capability.switch", title: "Light or switch", required: false, multiple: true + input "lock", "capability.lock", title: "Lock", required: false, multiple: true + input "garageDoor", "capability.doorControl", title: "Garage door controller", required: false, multiple: true + } + } + + page(name: "pageTwo", title: "Master devices", install: true, uninstall: true) +} + +def pageTwo() { + dynamicPage(name: "pageTwo") { + section("If set, the state of these devices will be toggled each time the tag is touched, " + + "e.g. a light that's on will be turned off and one that's off will be turned on, " + + "other devices of the same type will be set to the same state as their master device. " + + "If no master is designated then the majority of devices of the same type will be used " + + "to determine whether to turn on or off the devices.") { + + if (switch1 || masterSwitch) { + input "masterSwitch", "enum", title: "Master switch", options: switch1.collect{[(it.id): it.displayName]}, required: false + } + if (lock || masterLock) { + input "masterLock", "enum", title: "Master lock", options: lock.collect{[(it.id): it.displayName]}, required: false + } + if (garageDoor || masterDoor) { + input "masterDoor", "enum", title: "Master door", options: garageDoor.collect{[(it.id): it.displayName]}, required: false + } + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe tag, "nfcTouch", touchHandler + subscribe app, touchHandler +} + +private currentStatus(devices, master, attribute) { + log.trace "currentStatus($devices, $master, $attribute)" + def result = null + if (master) { + result = devices.find{it.id == master}?.currentValue(attribute) + } + else { + def map = [:] + devices.each { + def value = it.currentValue(attribute) + map[value] = (map[value] ?: 0) + 1 + log.trace "$it.displayName: $value" + } + log.trace map + result = map.collect{it}.sort{it.value}[-1].key + } + log.debug "$attribute = $result" + result +} + +def touchHandler(evt) { + log.trace "touchHandler($evt.descriptionText)" + if (switch1) { + def status = currentStatus(switch1, masterSwitch, "switch") + switch1.each { + if (status == "on") { + it.off() + } + else { + it.on() + } + } + } + + if (lock) { + def status = currentStatus(lock, masterLock, "lock") + lock.each { + if (status == "locked") { + lock.unlock() + } + else { + lock.lock() + } + } + } + + if (garageDoor) { + def status = currentStatus(garageDoor, masterDoor, "status") + garageDoor.each { + if (status == "open") { + it.close() + } + else { + it.open() + } + } + } +} diff --git a/smartapps/smartthings/notify-me-when-it-opens.src/notify-me-when-it-opens.groovy b/smartapps/smartthings/notify-me-when-it-opens.src/notify-me-when-it-opens.groovy new file mode 100644 index 00000000000..d305166ee54 --- /dev/null +++ b/smartapps/smartthings/notify-me-when-it-opens.src/notify-me-when-it-opens.groovy @@ -0,0 +1,49 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Notify Me When It Opens + * + * Author: SmartThings + */ +definition( + name: "Notify Me When It Opens", + namespace: "smartthings", + author: "SmartThings", + description: "Get a push message sent to your phone when an open/close sensor is opened.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" +) + +preferences { + section("When the door opens..."){ + input "contact1", "capability.contactSensor", title: "Where?" + } +} + +def installed() +{ + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + log.trace "$evt.value: $evt, $settings" + + log.debug "$contact1 was opened, sending push message to user" + sendPush("Your ${contact1.label ?: contact1.name} was opened") +} diff --git a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy new file mode 100644 index 00000000000..a59f3440475 --- /dev/null +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -0,0 +1,151 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Notify Me When + * + * Author: SmartThings + * Date: 2013-03-20 + * + * Change Log: + * 1. Todd Wackford + * 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell. + */ +definition( + name: "Notify Me When", + namespace: "smartthings", + author: "SmartThings", + description: "Get a push notification or text message when any of a variety of SmartThings is activated. Supports button push, motion, contact, acceleration, moisture and presence sensors as well as switches.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" +) + +preferences { + section("Choose one or more, when..."){ + input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + } + section("Send this message (optional, sends standard status message if not specified)"){ + input "messageText", "text", title: "Message Text", required: false + } + section("Via a push notification and/or an SMS message"){ + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] + } + } + section("Minimum time between messages (optional, defaults to every message)") { + input "frequency", "decimal", title: "Minutes", required: false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(button, "button.pushed", eventHandler) //tw + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) +} + +def eventHandler(evt) { + log.debug "Notify got evt ${evt}" + if (frequency) { + def lastTime = state[evt.deviceId] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + sendMessage(evt) + } + } + else { + sendMessage(evt) + } +} + +private sendMessage(evt) { + def msg = messageText ?: defaultText(evt) + log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + + if (!phone || pushAndPhone != "No") { + log.debug "sending push" + sendPush(msg) + } + if (phone) { + log.debug "sending SMS" + sendSms(phone, msg) + } + } + if (frequency) { + state[evt.deviceId] = now() + } +} + +private defaultText(evt) { + if (evt.name == "presence") { + if (evt.value == "present") { + if (includeArticle) { + "$evt.linkText has arrived at the $location.name" + } + else { + "$evt.linkText has arrived at $location.name" + } + } + else { + if (includeArticle) { + "$evt.linkText has left the $location.name" + } + else { + "$evt.linkText has left $location.name" + } + } + } + else { + evt.descriptionText + } +} + +private getIncludeArticle() { + def name = location.name.toLowerCase() + def segs = name.split(" ") + !(["work","home"].contains(name) || (segs.size() > 1 && (["the","my","a","an"].contains(segs[0]) || segs[0].endsWith("'s")))) +} diff --git a/smartapps/smartthings/notify-me-with-hue.src/notify-me-with-hue.groovy b/smartapps/smartthings/notify-me-with-hue.src/notify-me-with-hue.groovy new file mode 100644 index 00000000000..8cfd2969e21 --- /dev/null +++ b/smartapps/smartthings/notify-me-with-hue.src/notify-me-with-hue.groovy @@ -0,0 +1,198 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Notify Me With Hue + * + * Author: SmartThings + * Date: 2014-01-20 + */ +definition( + name: "Notify Me With Hue", + namespace: "smartthings", + author: "SmartThings", + description: "Changes the color and brightness of Philips Hue bulbs when any of a variety of SmartThings is activated. Supports motion, contact, acceleration, moisture and presence sensors as well as switches.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png" +) + +preferences { + + section("Control these bulbs...") { + input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required:true, multiple:true + } + + section("Choose one or more, when..."){ + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + input "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true + input "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + + section("Choose light effects...") + { + input "color", "enum", title: "Hue Color?", required: false, multiple:false, options: ["Red","Green","Blue","Yellow","Orange","Purple","Pink"] + input "lightLevel", "enum", title: "Light Level?", required: false, options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]] + input "duration", "number", title: "Duration Seconds?", required: false + //input "turnOn", "enum", title: "Turn On when Off?", required: false, options: ["Yes","No"] + } + + section("Minimum time between messages (optional, defaults to every message)") { + input "frequency", "decimal", title: "Minutes", required: false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (frequency) { + def lastTime = state[evt.deviceId] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + } + else { + takeAction(evt) + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + + if (frequency) { + state[evt.deviceId] = now() + } + + def hueColor = 0 + if(color == "Blue") + hueColor = 70//60 + else if(color == "Green") + hueColor = 39//30 + else if(color == "Yellow") + hueColor = 25//16 + else if(color == "Orange") + hueColor = 10 + else if(color == "Purple") + hueColor = 75 + else if(color == "Pink") + hueColor = 83 + + + state.previous = [:] + + hues.each { + state.previous[it.id] = [ + "switch": it.currentValue("switch"), + "level" : it.currentValue("level"), + "hue": it.currentValue("hue"), + "saturation": it.currentValue("saturation"), + "color": it.currentValue("color") + ] + } + + log.debug "current values = $state.previous" + + def newValue = [hue: hueColor, saturation: 100, level: (lightLevel as Integer) ?: 100] + log.debug "new value = $newValue" + + hues*.setColor(newValue) + setTimer() +} + +def setTimer() +{ + if(!duration) //default to 10 seconds + { + log.debug "pause 10" + pause(10 * 1000) + log.debug "reset hue" + resetHue() + } + else if(duration < 10) + { + log.debug "pause $duration" + pause(duration * 1000) + log.debug "resetHue" + resetHue() + } + else + { + log.debug "runIn $duration, resetHue" + runIn(duration,"resetHue", [overwrite: false]) + } +} + + +def resetHue() +{ + hues.each { + it.setColor(state.previous[it.id]) + } +} \ No newline at end of file diff --git a/smartapps/smartthings/once-a-day.src/once-a-day.groovy b/smartapps/smartthings/once-a-day.src/once-a-day.groovy new file mode 100644 index 00000000000..1158e28b54c --- /dev/null +++ b/smartapps/smartthings/once-a-day.src/once-a-day.groovy @@ -0,0 +1,64 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Once a Day + * + * Author: SmartThings + * + * Turn on one or more switches at a specified time and turn them off at a later time. + */ + +definition( + name: "Once a Day", + namespace: "smartthings", + author: "SmartThings", + description: "Turn on one or more switches at a specified time and turn them off at a later time.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section("Select switches to control...") { + input name: "switches", type: "capability.switch", multiple: true + } + section("Turn them all on at...") { + input name: "startTime", title: "Turn On Time?", type: "time" + } + section("And turn them off at...") { + input name: "stopTime", title: "Turn Off Time?", type: "time" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + schedule(startTime, "startTimerCallback") + schedule(stopTime, "stopTimerCallback") + +} + +def updated(settings) { + unschedule() + schedule(startTime, "startTimerCallback") + schedule(stopTime, "stopTimerCallback") +} + +def startTimerCallback() { + log.debug "Turning on switches" + switches.on() + +} + +def stopTimerCallback() { + log.debug "Turning off switches" + switches.off() +} diff --git a/smartapps/smartthings/photo-burst-when.src/photo-burst-when.groovy b/smartapps/smartthings/photo-burst-when.src/photo-burst-when.groovy new file mode 100644 index 00000000000..ac58efbed27 --- /dev/null +++ b/smartapps/smartthings/photo-burst-when.src/photo-burst-when.groovy @@ -0,0 +1,91 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Photo Burst When... + * + * Author: SmartThings + * + * Date: 2013-09-30 + */ + +definition( + name: "Photo Burst When...", + namespace: "smartthings", + author: "SmartThings", + description: "Take a burst of photos and send a push notification when...", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/photo-burst-when.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/photo-burst-when@2x.png" +) + +preferences { + section("Choose one or more, when..."){ + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + } + section("Take a burst of pictures") { + input "camera", "capability.imageCapture" + input "burstCount", "number", title: "How many? (default 5)", defaultValue:5 + } + section("Then send this message in a push notification"){ + input "messageText", "text", title: "Message Text" + } + section("And as text message to this number (optional)"){ + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number", required: false + } + } + +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(contact, "contact.open", sendMessage) + subscribe(acceleration, "acceleration.active", sendMessage) + subscribe(motion, "motion.active", sendMessage) + subscribe(mySwitch, "switch.on", sendMessage) + subscribe(arrivalPresence, "presence.present", sendMessage) + subscribe(departurePresence, "presence.not present", sendMessage) +} + +def sendMessage(evt) { + log.debug "$evt.name: $evt.value, $messageText" + + camera.take() + (1..((burstCount ?: 5) - 1)).each { + camera.take(delay: (500 * it)) + } + + if (location.contactBookEnabled) { + sendNotificationToContacts(messageText, recipients) + } + else { + sendPush(messageText) + if (phone) { + sendSms(phone, messageText) + } + } +} diff --git a/smartapps/smartthings/power-allowance.src/power-allowance.groovy b/smartapps/smartthings/power-allowance.src/power-allowance.groovy new file mode 100644 index 00000000000..7ecf963c5b4 --- /dev/null +++ b/smartapps/smartthings/power-allowance.src/power-allowance.groovy @@ -0,0 +1,57 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Power Allowance + * + * Author: SmartThings + */ +definition( + name: "Power Allowance", + namespace: "smartthings", + author: "SmartThings", + description: "Save energy or restrict total time an appliance (like a curling iron or TV) can be in use. When a switch turns on, automatically turn it back off after a set number of minutes you specify.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section("When a switch turns on...") { + input "theSwitch", "capability.switch" + } + section("Turn it off how many minutes later?") { + input "minutesLater", "number", title: "When?" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribe(theSwitch, "switch.on", switchOnHandler, [filterEvents: false]) +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + subscribe(theSwitch, "switch.on", switchOnHandler, [filterEvents: false]) +} + +def switchOnHandler(evt) { + log.debug "Switch ${theSwitch} turned: ${evt.value}" + def delay = minutesLater * 60 + log.debug "Turning off in ${minutesLater} minutes (${delay}seconds)" + runIn(delay, turnOffSwitch) +} + +def turnOffSwitch() { + theSwitch.off() +} diff --git a/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy b/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy new file mode 100644 index 00000000000..af5fa721b1c --- /dev/null +++ b/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy @@ -0,0 +1,50 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Presence Change Push + * + * Author: SmartThings + */ +definition( + name: "Presence Change Push", + namespace: "smartthings", + author: "SmartThings", + description: "Get a push notification when a SmartSense Presence tag or smartphone arrives at or departs from a location.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence@2x.png" +) + +preferences { + section("When a presence sensor arrives or departs this location..") { + input "presence", "capability.presenceSensor", title: "Which sensor?" + } +} + +def installed() { + subscribe(presence, "presence", presenceHandler) +} + +def updated() { + unsubscribe() + subscribe(presence, "presence", presenceHandler) +} + +def presenceHandler(evt) { + if (evt.value == "present") { + log.debug "${presence.label ?: presence.name} has arrived at the ${location}" + sendPush("${presence.label ?: presence.name} has arrived at the ${location}") + } else if (evt.value == "not present") { + log.debug "${presence.label ?: presence.name} has left the ${location}" + sendPush("${presence.label ?: presence.name} has left the ${location}") + } +} diff --git a/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy b/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy new file mode 100644 index 00000000000..d4ad1f39f7c --- /dev/null +++ b/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy @@ -0,0 +1,68 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Presence Change Text + * + * Author: SmartThings + */ +definition( + name: "Presence Change Text", + namespace: "smartthings", + author: "SmartThings", + description: "Send me a text message when my presence status changes.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence@2x.png" +) + +preferences { + section("When a presence sensor arrives or departs this location..") { + input "presence", "capability.presenceSensor", title: "Which sensor?" + } + section("Send a text message to...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + + +def installed() { + subscribe(presence, "presence", presenceHandler) +} + +def updated() { + unsubscribe() + subscribe(presence, "presence", presenceHandler) +} + +def presenceHandler(evt) { + if (evt.value == "present") { + log.debug "${presence.label ?: presence.name} has arrived at the ${location}" + + if (location.contactBookEnabled) { + sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients) + } + else { + sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}") + } + } else if (evt.value == "not present") { + log.debug "${presence.label ?: presence.name} has left the ${location}" + + if (location.contactBookEnabled) { + sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients) + } + else { + sendSms(phone1, "${presence.label ?: presence.name} has left the ${location}") + } + } +} diff --git a/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy b/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy new file mode 100644 index 00000000000..f491c6cd362 --- /dev/null +++ b/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy @@ -0,0 +1,209 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Ridiculously Automated Garage Door + * + * Author: SmartThings + * Date: 2013-03-10 + * + * Monitors arrival and departure of car(s) and + * + * 1) opens door when car arrives, + * 2) closes door after car has departed (for N minutes), + * 3) opens door when car door motion is detected, + * 4) closes door when door was opened due to arrival and interior door is closed. + */ + +definition( + name: "Ridiculously Automated Garage Door", + namespace: "smartthings", + author: "SmartThings", + description: "Monitors arrival and departure of car(s) and 1) opens door when car arrives, 2) closes door after car has departed (for N minutes), 3) opens door when car door motion is detected, 4) closes door when door was opened due to arrival and interior door is closed.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact@2x.png" +) + +preferences { + + section("Garage door") { + input "doorSensor", "capability.contactSensor", title: "Which sensor?" + input "doorSwitch", "capability.momentary", title: "Which switch?" + input "openThreshold", "number", title: "Warn when open longer than (optional)",description: "Number of minutes", required: false + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false + } + } + section("Car(s) using this garage door") { + input "cars", "capability.presenceSensor", title: "Presence sensor", description: "Which car(s)?", multiple: true, required: false + input "carDoorSensors", "capability.accelerationSensor", title: "Car door sensor(s)", description: "Which car(s)?", multiple: true, required: false + } + section("Interior door (optional)") { + input "interiorDoorSensor", "capability.contactSensor", title: "Contact sensor?", required: false + } + section("False alarm threshold (defaults to 10 min)") { + input "falseAlarmThreshold", "number", title: "Number of minutes", required: false + } +} + +def installed() { + log.trace "installed()" + subscribe() +} + +def updated() { + log.trace "updated()" + unsubscribe() + subscribe() +} + +def subscribe() { + log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}" + subscribe(doorSensor, "contact", garageDoorContact) + + subscribe(cars, "presence", carPresence) + subscribe(carDoorSensors, "acceleration", accelerationActive) + + if (interiorDoorSensor) { + subscribe(interiorDoorSensor, "contact.closed", interiorDoorClosed) + } +} + +def doorOpenCheck() +{ + final thresholdMinutes = openThreshold + if (thresholdMinutes) { + def currentState = doorSensor.contactState + log.debug "doorOpenCheck" + if (currentState?.value == "open") { + log.debug "open for ${now() - currentState.date.time}, openDoorNotificationSent: ${state.openDoorNotificationSent}" + if (!state.openDoorNotificationSent && now() - currentState.date.time > thresholdMinutes * 60 *1000) { + def msg = "${doorSwitch.displayName} was been open for ${thresholdMinutes} minutes" + log.info msg + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush msg + if (phone) { + sendSms phone, msg + } + } + state.openDoorNotificationSent = true + } + } + else { + state.openDoorNotificationSent = false + } + } +} + +def carPresence(evt) +{ + log.info "$evt.name: $evt.value" + // time in which there must be no "not present" events in order to open the door + final openDoorAwayInterval = falseAlarmThreshold ? falseAlarmThreshold * 60 : 600 + + if (evt.value == "present") { + // A car comes home + + def car = getCar(evt) + def t0 = new Date(now() - (openDoorAwayInterval * 1000)) + def states = car.statesSince("presence", t0) + def recentNotPresentState = states.find{it.value == "not present"} + + if (recentNotPresentState) { + log.debug "Not opening ${doorSwitch.displayName} since car was not present at ${recentNotPresentState.date}, less than ${openDoorAwayInterval} sec ago" + } + else { + if (doorSensor.currentContact == "closed") { + openDoor() + sendPush "Opening garage door due to arrival of ${car.displayName}" + state.appOpenedDoor = now() + } + else { + log.debug "door already open" + } + } + } + else { + // A car departs + if (doorSensor.currentContact == "open") { + closeDoor() + log.debug "Closing ${doorSwitch.displayName} after departure" + sendPush("Closing ${doorSwitch.displayName} after departure") + + } + else { + log.debug "Not closing ${doorSwitch.displayName} because its already closed" + } + } +} + +def garageDoorContact(evt) +{ + log.info "garageDoorContact, $evt.name: $evt.value" + if (evt.value == "open") { + schedule("0 * * * * ?", "doorOpenCheck") + } + else { + unschedule("doorOpenCheck") + } +} + + +def interiorDoorClosed(evt) +{ + log.info "interiorContact, $evt.name: $evt.value" + + // time during which closing the interior door will shut the garage door, if the app opened it + final threshold = 15 * 60 * 1000 + if (state.appOpenedDoor && now() - state.appOpenedDoor < threshold) { + state.appOpenedDoor = 0 + closeDoor() + } + else { + log.debug "app didn't open door" + } +} + +def accelerationActive(evt) +{ + log.info "$evt.name: $evt.value" + + if (doorSensor.currentContact == "closed") { + log.debug "opening door when car door opened" + openDoor() + } +} + +private openDoor() +{ + if (doorSensor.currentContact == "closed") { + log.debug "opening door" + doorSwitch.push() + } +} + +private closeDoor() +{ + if (doorSensor.currentContact == "open") { + log.debug "closing door" + doorSwitch.push() + } +} + +private getCar(evt) +{ + cars.find{it.id == evt.deviceId} +} diff --git a/smartapps/smartthings/rise-and-shine.src/rise-and-shine.groovy b/smartapps/smartthings/rise-and-shine.src/rise-and-shine.groovy new file mode 100644 index 00000000000..947067bc32b --- /dev/null +++ b/smartapps/smartthings/rise-and-shine.src/rise-and-shine.groovy @@ -0,0 +1,129 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Rise and Shine + * + * Author: SmartThings + * Date: 2013-03-07 + */ + +definition( + name: "Rise and Shine", + namespace: "smartthings", + author: "SmartThings", + description: "Changes mode when someone wakes up after a set time in the morning.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine@2x.png" +) + +preferences { + section("When there's motion on any of these sensors") { + input "motionSensors", "capability.motionSensor", multiple: true + } + section("During this time window (default End Time is 4:00 PM)") { + input "timeOfDay", "time", title: "Start Time?" + input "endTime", "time", title: "End Time?", required: false + } + section("Change to this mode") { + input "newMode", "mode", title: "Mode?" + } + section("And (optionally) turn on these appliances") { + input "switches", "capability.switch", multiple: true, required: false + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phoneNumber", "phone", title: "Send a Text Message?", required: false + } + } +} + +def installed() { + log.debug "installed, current mode = ${location.mode}, state.actionTakenOn = ${state.actionTakenOn}" + initialize() +} + +def updated() { + log.debug "updated, current mode = ${location.mode}, state.actionTakenOn = ${state.actionTakenOn}" + unsubscribe() + initialize() +} + +def initialize() { + log.trace "timeOfDay: $timeOfDay, endTime: $endTime" + subscribe(motionSensors, "motion.active", motionActiveHandler) + subscribe(location, modeChangeHandler) + if (state.modeStartTime == null) { + state.modeStartTime = 0 + } +} + +def modeChangeHandler(evt) { + state.modeStartTime = now() +} + +def motionActiveHandler(evt) +{ + // for backward compatibility + if (state.modeStartTime == null) { + subscribe(location, modeChangeHandler) + state.modeStartTime = 0 + } + + def t0 = now() + def modeStartTime = new Date(state.modeStartTime) + def timeZone = location.timeZone ?: timeZone(timeOfDay) + def startTime = timeTodayAfter(modeStartTime, timeOfDay, timeZone) + def endTime = timeTodayAfter(startTime, endTime ?: "16:00", timeZone) + log.debug "startTime: $startTime, endTime: $endTime, t0: ${new Date(t0)}, modeStartTime: ${modeStartTime}, actionTakenOn: $state.actionTakenOn, currentMode: $location.mode, newMode: $newMode " + + if (t0 >= startTime.time && t0 <= endTime.time && location.mode != newMode) { + def message = "Good morning! SmartThings changed the mode to '$newMode'" + send(message) + setLocationMode(newMode) + log.debug message + + def dateString = new Date().format("yyyy-MM-dd") + log.debug "last turned on switches on ${state.actionTakenOn}, today is ${dateString}" + if (state.actionTakenOn != dateString) { + log.debug "turning on switches" + state.actionTakenOn = dateString + switches?.on() + } + + } + else { + log.debug "not in time window, or mode is already set, currentMode = ${location.mode}, newMode = $newMode" + } +} + +private send(msg) { + + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phoneNumber) { + log.debug("sending text message") + sendSms(phoneNumber, msg) + } + } + + log.debug msg +} diff --git a/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy new file mode 100644 index 00000000000..aa6a22854ce --- /dev/null +++ b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy @@ -0,0 +1,420 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Samsung TV Service Manager + * + * Author: SmartThings (Juan Risso) + */ + +definition( + name: "Samsung TV (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png" +) + +preferences { + page(name:"samsungDiscovery", title:"Samsung TV Setup", content:"samsungDiscovery", refreshTimeout:5) +} + +def getDeviceType() { + return "urn:samsung.com:device:RemoteControlReceiver:1" +} + +//PAGES +def samsungDiscovery() +{ + if(canInstallLabs()) + { + int samsungRefreshCount = !state.samsungRefreshCount ? 0 : state.samsungRefreshCount as int + state.samsungRefreshCount = samsungRefreshCount + 1 + def refreshInterval = 3 + + def options = samsungesDiscovered() ?: [] + + def numFound = options.size() ?: 0 + + if(!state.subscribe) { + log.trace "subscribe to location" + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //samsung discovery request every 5 //25 seconds + if((samsungRefreshCount % 5) == 0) { + log.trace "Discovering..." + discoversamsunges() + } + + //setup.xml request every 3 seconds except on discoveries + if(((samsungRefreshCount % 1) == 0) && ((samsungRefreshCount % 8) != 0)) { + log.trace "Verifing..." + verifysamsungPlayer() + } + + return dynamicPage(name:"samsungDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { + section("Please wait while we discover your Samsung TV. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { + input "selectedsamsung", "enum", required:false, title:"Select Samsung TV (${numFound} found)", multiple:true, options:options + } + } + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"samsungDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) { + section("Upgrade") { + paragraph "$upgradeNeeded" + } + } + } +} + +def installed() { + log.trace "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.trace "Updated with settings: ${settings}" + unschedule() + initialize() +} + +def uninstalled() { + def devices = getChildDevices() + log.trace "deleting ${devices.size()} samsung" + devices.each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def initialize() { + // remove location subscription afterwards + if (selectedsamsung) { + addsamsung() + } + //Check every 5 minutes for IP change + runEvery5Minutes("discoversamsunges") +} + +//CHILD DEVICE METHODS +def addsamsung() { + def players = getVerifiedsamsungPlayer() + log.trace "Adding childs" + selectedsamsung.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newPlayer = players.find { (it.value.ip + ":" + it.value.port) == dni } + log.trace "newPlayer = $newPlayer" + log.trace "dni = $dni" + d = addChildDevice("smartthings", "Samsung Smart TV", dni, newPlayer?.value.hub, [label:"${newPlayer?.value.name}"]) + log.trace "created ${d.displayName} with id $dni" + + d.setModel(newPlayer?.value.model) + log.trace "setModel to ${newPlayer?.value.model}" + } else { + log.trace "found ${d.displayName} with id $dni already exists" + } + } +} + +private tvAction(key,deviceNetworkId) { + log.debug "Executing ${tvCommand}" + + def tvs = getVerifiedsamsungPlayer() + def thetv = tvs.find { (it.value.ip + ":" + it.value.port) == deviceNetworkId } + + // Standard Connection Data + def appString = "iphone..iapp.samsung" + def appStringLength = appString.getBytes().size() + + def tvAppString = "iphone.UN60ES8000.iapp.samsung" + def tvAppStringLength = tvAppString.getBytes().size() + + def remoteName = "SmartThings".encodeAsBase64().toString() + def remoteNameLength = remoteName.getBytes().size() + + // Device Connection Data + def ipAddress = convertHexToIP(thetv?.value.ip).encodeAsBase64().toString() + def ipAddressHex = deviceNetworkId.substring(0,8) + def ipAddressLength = ipAddress.getBytes().size() + + def macAddress = thetv?.value.mac.encodeAsBase64().toString() + def macAddressLength = macAddress.getBytes().size() + + // The Authentication Message + def authenticationMessage = "${(char)0x64}${(char)0x00}${(char)ipAddressLength}${(char)0x00}${ipAddress}${(char)macAddressLength}${(char)0x00}${macAddress}${(char)remoteNameLength}${(char)0x00}${remoteName}" + def authenticationMessageLength = authenticationMessage.getBytes().size() + + def authenticationPacket = "${(char)0x00}${(char)appStringLength}${(char)0x00}${appString}${(char)authenticationMessageLength}${(char)0x00}${authenticationMessage}" + + // If our initial run, just send the authentication packet so the prompt appears on screen + if (key == "AUTHENTICATE") { + sendHubCommand(new physicalgraph.device.HubAction(authenticationPacket, physicalgraph.device.Protocol.LAN, "${ipAddressHex}:D6D8")) + } else { + // Build the command we will send to the Samsung TV + def command = "KEY_${key}".encodeAsBase64().toString() + def commandLength = command.getBytes().size() + + def actionMessage = "${(char)0x00}${(char)0x00}${(char)0x00}${(char)commandLength}${(char)0x00}${command}" + def actionMessageLength = actionMessage.getBytes().size() + + def actionPacket = "${(char)0x00}${(char)tvAppStringLength}${(char)0x00}${tvAppString}${(char)actionMessageLength}${(char)0x00}${actionMessage}" + + // Send both the authentication and action at the same time + sendHubCommand(new physicalgraph.device.HubAction(authenticationPacket + actionPacket, physicalgraph.device.Protocol.LAN, "${ipAddressHex}:D6D8")) + } +} + +private discoversamsunges() +{ + sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN)) +} + + +private verifysamsungPlayer() { + def devices = getsamsungPlayer().findAll { it?.value?.verified != true } + + if(devices) { + log.warn "UNVERIFIED PLAYERS!: $devices" + } + + devices.each { + verifysamsung((it?.value?.ip + ":" + it?.value?.port), it?.value?.ssdpPath) + } +} + +private verifysamsung(String deviceNetworkId, String devicessdpPath) { + log.trace "dni: $deviceNetworkId, ssdpPath: $devicessdpPath" + String ip = getHostAddress(deviceNetworkId) + log.trace "ip:" + ip + sendHubCommand(new physicalgraph.device.HubAction("""GET ${devicessdpPath} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) +} + +Map samsungesDiscovered() { + def vsamsunges = getVerifiedsamsungPlayer() + def map = [:] + vsamsunges.each { + def value = "${it.value.name}" + def key = it.value.ip + ":" + it.value.port + map["${key}"] = value + } + log.trace "Devices discovered $map" + map +} + +def getsamsungPlayer() +{ + state.samsunges = state.samsunges ?: [:] +} + +def getVerifiedsamsungPlayer() +{ + getsamsungPlayer().findAll{ it?.value?.verified == true } +} + +def locationHandler(evt) { + def description = evt.description + def hub = evt?.hubId + def parsedEvent = parseEventMessage(description) + parsedEvent << ["hub":hub] + log.trace "${parsedEvent}" + log.trace "${getDeviceType()} - ${parsedEvent.ssdpTerm}" + if (parsedEvent?.ssdpTerm?.contains(getDeviceType())) + { //SSDP DISCOVERY EVENTS + + log.trace "TV found" + def samsunges = getsamsungPlayer() + + if (!(samsunges."${parsedEvent.ssdpUSN.toString()}")) + { //samsung does not exist + log.trace "Adding Device to state..." + samsunges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } + else + { // update the values + + log.trace "Device was already found in state..." + + def d = samsunges."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + log.trace "Device's port or ip changed..." + } + + if (deviceChangedValues) { + def children = getChildDevices() + children.each { + if (it.getDeviceDataByName("mac") == parsedEvent.mac) { + log.trace "updating dni for device ${it} with mac ${parsedEvent.mac}" + it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists + } + } + } + } + } + else if (parsedEvent.headers && parsedEvent.body) + { // samsung RESPONSES + def deviceHeaders = parseLanMessage(description, false) + def type = deviceHeaders.headers."content-type" + def body + log.trace "REPONSE TYPE: $type" + if (type?.contains("xml")) + { // description.xml response (application/xml) + body = new XmlSlurper().parseText(deviceHeaders.body) + log.debug body.device.deviceType.text() + if (body?.device?.deviceType?.text().contains(getDeviceType())) + { + def samsunges = getsamsungPlayer() + def player = samsunges.find {it?.key?.contains(body?.device?.UDN?.text())} + if (player) + { + player.value << [name:body?.device?.friendlyName?.text(),model:body?.device?.modelName?.text(), serialNumber:body?.device?.serialNum?.text(), verified: true] + } + else + { + log.error "The xml file returned a device that didn't exist" + } + } + } + else if(type?.contains("json")) + { //(application/json) + body = new groovy.json.JsonSlurper().parseText(bodyString) + log.trace "GOT JSON $body" + } + + } + else { + log.trace "TV not found..." + //log.trace description + } +} + +private def parseEventMessage(String description) { + def event = [:] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('devicetype:')) { + def valueString = part.split(":")[1].trim() + event.devicetype = valueString + } + else if (part.startsWith('mac:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + event.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) { + event.ssdpUSN = valueString + } + } + else if (part.startsWith('ssdpTerm:')) { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) { + event.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) { + part -= "headers:" + def valueString = part.trim() + if (valueString) { + event.headers = valueString + } + } + else if (part.startsWith('body')) { + part -= "body:" + def valueString = part.trim() + if (valueString) { + event.body = valueString + } + } + } + event +} + +def parse(childDevice, description) { + def parsedEvent = parseEventMessage(description) + + if (parsedEvent.headers && parsedEvent.body) { + def headerString = new String(parsedEvent.headers.decodeBase64()) + def bodyString = new String(parsedEvent.body.decodeBase64()) + log.trace "parse() - ${bodyString}" + + def body = new groovy.json.JsonSlurper().parseText(bodyString) + } else { + log.trace "parse - got something other than headers,body..." + return [] + } +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +private getHostAddress(d) { + def parts = d.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +private Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} \ No newline at end of file diff --git a/smartapps/smartthings/scheduled-mode-change.src/scheduled-mode-change.groovy b/smartapps/smartthings/scheduled-mode-change.src/scheduled-mode-change.groovy new file mode 100644 index 00000000000..6b2350557e7 --- /dev/null +++ b/smartapps/smartthings/scheduled-mode-change.src/scheduled-mode-change.groovy @@ -0,0 +1,94 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Scheduled Mode Change - Presence Optional + * + * Author: SmartThings + + * + */ + +definition( + name: "Scheduled Mode Change", + namespace: "smartthings", + author: "SmartThings", + description: "Changes mode at a specific time of day.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png" +) + +preferences { + section("At this time every day") { + input "time", "time", title: "Time of Day" + } + section("Change to this mode") { + input "newMode", "mode", title: "Mode?" + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phoneNumber", "phone", title: "Send a text message?", required: false + } + } +} + +def installed() { + initialize() +} + +def updated() { + unschedule() + initialize() +} + +def initialize() { + schedule(time, changeMode) +} + +def changeMode() { + log.debug "changeMode, location.mode = $location.mode, newMode = $newMode, location.modes = $location.modes" + if (location.mode != newMode) { + if (location.modes?.find{it.name == newMode}) { + setLocationMode(newMode) + send "${label} has changed the mode to '${newMode}'" + } + else { + send "${label} tried to change to undefined mode '${newMode}'" + } + } +} + +private send(msg) { + + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage == "Yes") { + log.debug("sending push message") + sendPush(msg) + } + + if (phoneNumber) { + log.debug("sending text message") + sendSms(phoneNumber, msg) + } + } + + log.debug msg +} + +private getLabel() { + app.label ?: "SmartThings" +} diff --git a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy new file mode 100644 index 00000000000..92581229862 --- /dev/null +++ b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy @@ -0,0 +1,85 @@ +/** + * Send HAM Bridge Command When… + * + * For more information about HAM Bridge please visit http://solutionsetcetera.com/HAMBridge/ + * + * Copyright 2014 Scottin Pollock + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Send HAM Bridge Command When…", + namespace: "soletc.com", + author: "Scottin Pollock", + description: "Sends a command to your HAM Bridge server when SmartThings are activated.", + category: "Convenience", + iconUrl: "http://solutionsetcetera.com/stuff/STIcons/HB.png", + iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png" +) + + +preferences { + section("Choose one or more, when..."){ + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + } + section("Send this command to HAM Bridge"){ + input "HAMBcommand", "text", title: "Command to send", required: true + } + section("Server address and port number"){ + input "server", "text", title: "Server IP", description: "Your HAM Bridger Server IP", required: true + input "port", "number", title: "Port", description: "Port Number", required: true + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) +} + +def eventHandler(evt) { + sendHttp() +} + +def sendHttp() { +def ip = "${settings.server}:${settings.port}" +def deviceNetworkId = "1234" +sendHubCommand(new physicalgraph.device.HubAction("""GET /?${settings.HAMBcommand} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) +} \ No newline at end of file diff --git a/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy new file mode 100644 index 00000000000..f27671c3600 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy @@ -0,0 +1,126 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Severe Weather Alert + * + * Author: SmartThings + * Date: 2013-03-04 + */ +definition( + name: "Severe Weather Alert", + namespace: "smartthings", + author: "SmartThings", + description: "Get a push notification when severe weather is in your area.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather@2x.png" +) + +preferences { + section ("In addition to push notifications, send text alerts to...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone Number 1", required: false + input "phone2", "phone", title: "Phone Number 2", required: false + input "phone3", "phone", title: "Phone Number 3", required: false + } + } + + section ("Zip code (optional, defaults to location coordinates)...") { + input "zipcode", "text", title: "Zip Code", required: false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + scheduleJob() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unschedule() + scheduleJob() +} + +def scheduleJob() { + def sec = Math.round(Math.floor(Math.random() * 60)) + def min = Math.round(Math.floor(Math.random() * 60)) + def cron = "$sec $min * * * ?" + schedule(cron, "checkForSevereWeather") +} + +def checkForSevereWeather() { + def alerts + if(locationIsDefined()) { + if(zipcodeIsValid()) { + alerts = getWeatherFeature("alerts", zipcode)?.alerts + } else { + log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode" + alerts = getWeatherFeature("alerts")?.alerts + } + } else { + log.warn "Severe Weather Alert: Location is not defined" + } + + def newKeys = alerts?.collect{it.type + it.date_epoch} ?: [] + log.debug "Severe Weather Alert: newKeys: $newKeys" + + def oldKeys = state.alertKeys ?: [] + log.debug "Severe Weather Alert: oldKeys: $oldKeys" + + if (newKeys != oldKeys) { + + state.alertKeys = newKeys + + alerts.each {alert -> + if (!oldKeys.contains(alert.type + alert.date_epoch) && descriptionFilter(alert.description)) { + def msg = "Weather Alert! ${alert.description} from ${alert.date} until ${alert.expires}" + send(msg) + } + } + } +} + +def descriptionFilter(String description) { + def filterList = ["special", "statement", "test"] + def passesFilter = true + filterList.each() { word -> + if(description.toLowerCase().contains(word)) { passesFilter = false } + } + passesFilter +} + +def locationIsDefined() { + zipcodeIsValid() || location.zipCode || ( location.latitude && location.longitude ) +} + +def zipcodeIsValid() { + zipcode && zipcode.isNumber() && zipcode.size() == 5 +} + +private send(message) { + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + sendPush message + if (settings.phone1) { + sendSms phone1, message + } + if (settings.phone2) { + sendSms phone2, message + } + if (settings.phone3) { + sendSms phone3, message + } + } +} diff --git a/smartapps/smartthings/single-button-controller.src/single-button-controller.groovy b/smartapps/smartthings/single-button-controller.src/single-button-controller.groovy new file mode 100644 index 00000000000..1d65e23a5b4 --- /dev/null +++ b/smartapps/smartthings/single-button-controller.src/single-button-controller.groovy @@ -0,0 +1,151 @@ +/** + * Button Controller + * + * Copyright 2014 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. + * + */ +definition(name: "Single Button Controller", + namespace: "smartthings", + author: "SmartThings", + description: "Use your Aeon Panic Button to setup events when the button is used", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + category: "Reviewers") + +preferences { + page(name: "selectButton") +} + +def selectButton() { + dynamicPage(name: "selectButton", title: "First, select your button device", install: true, uninstall: configured()) { + section { + input "buttonDevice", "device.aeonKeyFob", title: "Button", multiple: false, required: true + } + section("Lights") { + input "lights_1_pushed", "capability.switch", title: "Pushed", multiple: true, required: false + input "lights_1_held", "capability.switch", title: "Held", multiple: true, required: false + } + section("Locks") { + input "locks_1_pushed", "capability.lock", title: "Pushed", multiple: true, required: false + input "locks_1_held", "capability.lock", title: "Held", multiple: true, required: false + } + section("Sonos") { + input "sonos_1_pushed", "capability.musicPlayer", title: "Pushed", multiple: true, required: false + input "sonos_1_held", "capability.musicPlayer", title: "Held", multiple: true, required: false + } + section("Modes") { + input "mode_1_pushed", "mode", title: "Pushed", required: false + input "mode_1_held", "mode", title: "Held", required: false + } + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + section("Hello Home Actions") { + log.trace phrases + input "phrase_1_pushed", "enum", title: "Pushed", required: false, options: phrases + input "phrase_1_held", "enum", title: "Held", required: false, options: phrases + } + } + } +} + +def installed() { + initialize() +} + +def updated() { + unsubscribe() + initialize() +} + +def initialize() { + subscribe(buttonDevice, "button", buttonEvent) +} + +def configured() { + return buttonDevice || buttonConfigured(1) +} + +def buttonConfigured(idx) { + return settings["lights_$idx_pushed"] || + settings["locks_$idx_pushed"] || + settings["sonos_$idx_pushed"] || + settings["mode_$idx_pushed"] +} + +def buttonEvent(evt){ + def buttonNumber = evt.data // why doesn't jsonData work? always returning [:] + def value = evt.value + log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)" + log.debug "button: $buttonNumber, value: $value" + + def recentEvents = buttonDevice.eventsSince(new Date(now() - 3000)).findAll{it.value == evt.value} + log.debug "Found ${recentEvents.size()?:0} events in past 3 seconds" + + executeHandlers(1, value) +} + +def executeHandlers(buttonNumber, value) { + log.debug "executeHandlers: $buttonNumber - $value" + + def lights = find('lights', buttonNumber, value) + if (lights != null) toggle(lights) + + def locks = find('locks', buttonNumber, value) + if (locks != null) toggle(locks) + + def sonos = find('sonos', buttonNumber, value) + if (sonos != null) toggle(sonos) + + def mode = find('mode', buttonNumber, value) + if (mode != null) changeMode(mode) + + def phrase = find('phrase', buttonNumber, value) + if (phrase != null) location.helloHome.execute(phrase) +} + +def find(type, buttonNumber, value) { + def preferenceName = type + "_" + buttonNumber + "_" + value + def pref = settings[preferenceName] + if(pref != null) { + log.debug "Found: $pref for $preferenceName" + } + + return pref +} + +def toggle(devices) { + log.debug "toggle: $devices = ${devices*.currentValue('switch')}" + + if (devices*.currentValue('switch').contains('on')) { + devices.off() + } + else if (devices*.currentValue('switch').contains('off')) { + devices.on() + } + else if (devices*.currentValue('lock').contains('locked')) { + devices.unlock() + } + else if (devices*.currentValue('lock').contains('unlocked')) { + devices.lock() + } + else { + devices.on() + } +} + +def changeMode(mode) { + log.debug "changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes" + + if (location.mode != mode && location.modes?.find { it.name == mode }) { + setLocationMode(mode) + } +} diff --git a/smartapps/smartthings/sleepy-time.src/sleepy-time.groovy b/smartapps/smartthings/sleepy-time.src/sleepy-time.groovy new file mode 100644 index 00000000000..edec40c37d8 --- /dev/null +++ b/smartapps/smartthings/sleepy-time.src/sleepy-time.groovy @@ -0,0 +1,89 @@ +/** + * Sleepy Time + * + * Copyright 2014 Physical Graph Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Sleepy Time", + namespace: "smartthings", + author: "SmartThings", + description: "Use Jawbone sleep mode events to automatically execute Hello, Home phrases. Automatially put the house to bed or wake it up in the morning by pushing the button on your UP.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png" +) + +preferences { + page(name: "selectPhrases") +} + +def selectPhrases() { + dynamicPage(name: "selectPhrases", title: "Configure Your Jawbone Phrases.", install: true, uninstall: true) { + section("Select your Jawbone UP") { + input "jawbone", "device.jawboneUser", title: "Jawbone UP", required: true, multiple: false, submitOnChange:true + } + + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + section("Hello Home Actions") { + log.trace phrases + input "sleepPhrase", "enum", title: "Enter Sleep Mode (Bedtime) Phrase", required: false, options: phrases, submitOnChange:true + input "wakePhrase", "enum", title: "Exit Sleep Mode (Waking Up) Phrase", required: false, options: phrases, submitOnChange:true + } + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() + + log.debug "Subscribing to sleeping events." + + subscribe (jawbone, "sleeping", jawboneHandler) + +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + + log.debug "Subscribing to sleeping events." + + subscribe (jawbone, "sleeping", jawboneHandler) + + initialize() +} + +def initialize() { + // TODO: subscribe to attributes, devices, locations, etc. +} + +def jawboneHandler(evt) { + log.debug "In Jawbone Event Handler, Event Name = ${evt.name}, Value = ${evt.value}" + if (evt.value == "sleeping" && sleepPhrase) { + log.debug "Sleeping" + sendNotificationEvent("Sleepy Time performing \"${sleepPhrase}\" for you as requested.") + location.helloHome.execute(settings.sleepPhrase) + } + else if (evt.value == "not sleeping" && wakePhrase) { + log.debug "Awake" + sendNotificationEvent("Sleepy Time performing \"${wakePhrase}\" for you as requested.") + location.helloHome.execute(settings.wakePhrase) + } + +} \ No newline at end of file diff --git a/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy b/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy new file mode 100644 index 00000000000..5afb0beea5a --- /dev/null +++ b/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy @@ -0,0 +1,174 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Smart Nightlight + * + * Author: SmartThings + * + */ +definition( + name: "Smart Nightlight", + namespace: "smartthings", + author: "SmartThings", + description: "Turns on lights when it's dark and motion is detected. Turns lights off when it becomes light or some time after motion ceases.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-luminance.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-luminance@2x.png" +) + +preferences { + section("Control these lights..."){ + input "lights", "capability.switch", multiple: true + } + section("Turning on when it's dark and there's movement..."){ + input "motionSensor", "capability.motionSensor", title: "Where?" + } + section("And then off when it's light or there's been no movement for..."){ + input "delayMinutes", "number", title: "Minutes?" + } + section("Using either on this light sensor (optional) or the local sunrise and sunset"){ + input "lightSensor", "capability.illuminanceMeasurement", required: false + } + section ("Sunrise offset (optional)...") { + input "sunriseOffsetValue", "text", title: "HH:MM", required: false + input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] + } + section ("Sunset offset (optional)...") { + input "sunsetOffsetValue", "text", title: "HH:MM", required: false + input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] + } + section ("Zip code (optional, defaults to location coordinates when location services are enabled)...") { + input "zipCode", "text", title: "Zip code", required: false + } +} + +def installed() { + initialize() +} + +def updated() { + unsubscribe() + unschedule() + initialize() +} + +def initialize() { + subscribe(motionSensor, "motion", motionHandler) + if (lightSensor) { + subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false]) + } + else { + subscribe(location, "position", locationPositionChange) + subscribe(location, "sunriseTime", sunriseSunsetTimeHandler) + subscribe(location, "sunsetTime", sunriseSunsetTimeHandler) + astroCheck() + } +} + +def locationPositionChange(evt) { + log.trace "locationChange()" + astroCheck() +} + +def sunriseSunsetTimeHandler(evt) { + state.lastSunriseSunsetEvent = now() + log.debug "SmartNightlight.sunriseSunsetTimeHandler($app.id)" + astroCheck() +} + +def motionHandler(evt) { + log.debug "$evt.name: $evt.value" + if (evt.value == "active") { + if (enabled()) { + log.debug "turning on lights due to motion" + lights.on() + state.lastStatus = "on" + } + state.motionStopTime = null + } + else { + state.motionStopTime = now() + if(delayMinutes) { + runIn(delayMinutes*60, turnOffMotionAfterDelay, [overwrite: false]) + } else { + turnOffMotionAfterDelay() + } + } +} + +def illuminanceHandler(evt) { + log.debug "$evt.name: $evt.value, lastStatus: $state.lastStatus, motionStopTime: $state.motionStopTime" + def lastStatus = state.lastStatus + if (lastStatus != "off" && evt.integerValue > 50) { + lights.off() + state.lastStatus = "off" + } + else if (state.motionStopTime) { + if (lastStatus != "off") { + def elapsed = now() - state.motionStopTime + if (elapsed >= ((delayMinutes ?: 0) * 60000L) - 2000) { + lights.off() + state.lastStatus = "off" + } + } + } + else if (lastStatus != "on" && evt.integerValue < 30){ + lights.on() + state.lastStatus = "on" + } +} + +def turnOffMotionAfterDelay() { + log.trace "In turnOffMotionAfterDelay, state.motionStopTime = $state.motionStopTime, state.lastStatus = $state.lastStatus" + if (state.motionStopTime && state.lastStatus != "off") { + def elapsed = now() - state.motionStopTime + log.trace "elapsed = $elapsed" + if (elapsed >= ((delayMinutes ?: 0) * 60000L) - 2000) { + log.debug "Turning off lights" + lights.off() + state.lastStatus = "off" + } + } +} + +def scheduleCheck() { + log.debug "In scheduleCheck - skipping" + //turnOffMotionAfterDelay() +} + +def astroCheck() { + def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) + state.riseTime = s.sunrise.time + state.setTime = s.sunset.time + log.debug "rise: ${new Date(state.riseTime)}($state.riseTime), set: ${new Date(state.setTime)}($state.setTime)" +} + +private enabled() { + def result + if (lightSensor) { + result = lightSensor.currentIlluminance?.toInteger() < 30 + } + else { + def t = now() + result = t < state.riseTime || t > state.setTime + } + result +} + +private getSunriseOffset() { + sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null +} + +private getSunsetOffset() { + sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null +} + diff --git a/smartapps/smartthings/smart-security.src/smart-security.groovy b/smartapps/smartthings/smart-security.src/smart-security.groovy new file mode 100644 index 00000000000..6d15cffbc8f --- /dev/null +++ b/smartapps/smartthings/smart-security.src/smart-security.groovy @@ -0,0 +1,317 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Smart Security + * + * Author: SmartThings + * Date: 2013-03-07 + */ +definition( + name: "Smart Security", + namespace: "smartthings", + author: "SmartThings", + description: "Alerts you when there are intruders but not when you just got up for a glass of water in the middle of the night", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe@2x.png" +) + +preferences { + section("Sensors detecting an intruder") { + input "intrusionMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false + input "intrusionContacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false + } + section("Sensors detecting residents") { + input "residentMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false + } + section("Alarm settings and actions") { + input "alarms", "capability.alarm", title: "Which Alarm(s)", multiple: true, required: false + input "silent", "enum", options: ["Yes","No"], title: "Silent alarm only (Yes/No)" + input "seconds", "number", title: "Delay in seconds before siren sounds" + input "lights", "capability.switch", title: "Flash these lights (optional)", multiple: true, required: false + input "newMode", "mode", title: "Change to this mode (optional)", required: false + } + section("Notify others (optional)") { + input "textMessage", "text", title: "Send this message", multiple: false, required: false + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "To this phone", multiple: false, required: false + } + } + section("Arm system when residents quiet for (default 3 minutes)") { + input "residentsQuietThreshold", "number", title: "Time in minutes", required: false + } +} + +def installed() { + log.debug "INSTALLED" + subscribeToEvents() + state.alarmActive = null +} + +def updated() { + log.debug "UPDATED" + unsubscribe() + subscribeToEvents() + unschedule() + state.alarmActive = null + state.residentsAreUp = null + state.lastIntruderMotion = null + alarms?.off() +} + +private subscribeToEvents() +{ + subscribe intrusionMotions, "motion", intruderMotion + subscribe residentMotions, "motion", residentMotion + subscribe intrusionContacts, "contact", contact + subscribe alarms, "alarm", alarm + subscribe(app, appTouch) +} + +private residentsHaveBeenQuiet() +{ + def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000 + def result = true + def t0 = new Date(now() - threshold) + for (sensor in residentMotions) { + def recentStates = sensor.statesSince("motion", t0) + if (recentStates.find{it.value == "active"}) { + result = false + break + } + } + log.debug "residentsHaveBeenQuiet: $result" + result +} + +private intruderMotionInactive() +{ + def result = true + for (sensor in intrusionMotions) { + if (sensor.currentMotion == "active") { + result = false + break + } + } + result +} + +private isResidentMotionSensor(evt) +{ + residentMotions?.find{it.id == evt.deviceId} != null +} + +def appTouch(evt) +{ + alarms?.off() + state.alarmActive = false +} + +// Here to handle old subscriptions +def motion(evt) +{ + if (isResidentMotionSensor(evt)) { + log.debug "resident motion, $evt.name: $evt.value" + residentMotion(evt) + } + else { + log.debug "intruder motion, $evt.name: $evt.value" + intruderMotion(evt) + } +} + +def intruderMotion(evt) +{ + if (evt.value == "active") { + log.debug "motion by potential intruder, residentsAreUp: $state.residentsAreUp" + if (!state.residentsAreUp) { + log.trace "checking if residents have been quiet" + if (residentsHaveBeenQuiet()) { + log.trace "calling startAlarmSequence" + startAlarmSequence() + } + else { + log.trace "calling disarmIntrusionDetection" + disarmIntrusionDetection() + } + } + } + state.lastIntruderMotion = now() +} + +def residentMotion(evt) +{ + // Don't think we need this any more + //if (evt.value == "inactive") { + // if (state.residentsAreUp) { + // startReArmSequence() + // } + //} +} + +def contact(evt) +{ + if (evt.value == "open") { + // TODO - check for residents being up? + if (!state.residentsAreUp) { + if (residentsHaveBeenQuiet()) { + startAlarmSequence() + } + else { + disarmIntrusionDetection() + } + } + } +} + +def alarm(evt) +{ + log.debug "$evt.name: $evt.value" + if (evt.value == "off") { + alarms?.off() + state.alarmActive = false + } +} + +private disarmIntrusionDetection() +{ + log.debug "residents are up, disarming intrusion detection" + state.residentsAreUp = true + scheduleReArmCheck() +} + +private scheduleReArmCheck() +{ + def cron = "0 * * * * ?" + schedule(cron, "checkForReArm") + log.debug "Starting re-arm check, cron: $cron" +} + +def checkForReArm() +{ + def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000 + log.debug "checkForReArm: threshold is $threshold" + // check last intruder motion + def lastIntruderMotion = state.lastIntruderMotion + log.debug "checkForReArm: lastIntruderMotion=$lastIntruderMotion" + if (lastIntruderMotion != null) + { + log.debug "checkForReArm, time since last intruder motion: ${now() - lastIntruderMotion}" + if (now() - lastIntruderMotion > threshold) { + log.debug "re-arming intrusion detection" + state.residentsAreUp = false + unschedule() + } + } + else { + log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection" + } +} + +private startAlarmSequence() +{ + if (state.alarmActive) { + log.debug "alarm already active" + } + else { + state.alarmActive = true + log.debug "starting alarm sequence" + + sendPush("Potential intruder detected!") + + if (newMode) { + setLocationMode(newMode) + } + + if (silentAlarm()) { + log.debug "Silent alarm only" + alarms?.strobe() + if (location.contactBookEnabled) { + sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients) + } + else { + if (phone) { + sendSms(phone, textMessage ?: "Potential intruder detected") + } + } + } + else { + def delayTime = seconds + if (delayTime) { + alarms?.strobe() + runIn(delayTime, "soundSiren") + log.debug "Sounding siren in $delayTime seconds" + } + else { + soundSiren() + } + } + + if (lights) { + flashLights(Math.min((seconds/2) as Integer, 10)) + } + } +} + +def soundSiren() +{ + if (state.alarmActive) { + log.debug "Sounding siren" + if (location.contactBookEnabled) { + sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients) + } + else { + if (phone) { + sendSms(phone, textMessage ?: "Potential intruder detected") + } + } + alarms?.both() + if (lights) { + log.debug "continue flashing lights" + continueFlashing() + } + } + else { + log.debug "alarm activation aborted" + } + unschedule("soundSiren") // Temporary work-around to scheduling bug +} + +def continueFlashing() +{ + unschedule() + if (state.alarmActive) { + flashLights(10) + schedule(util.cronExpression(now() + 10000), "continueFlashing") + } +} + +private flashLights(numFlashes) { + def onFor = 1000 + def offFor = 1000 + + log.debug "FLASHING $numFlashes times" + def delay = 1L + numFlashes.times { + log.trace "Switch on after $delay msec" + lights?.on(delay: delay) + delay += onFor + log.trace "Switch off after $delay msec" + lights?.off(delay: delay) + delay += offFor + } +} + +private silentAlarm() +{ + silent?.toLowerCase() in ["yes","true","y"] +} diff --git a/smartapps/smartthings/smartweather-station-controller.src/smartweather-station-controller.groovy b/smartapps/smartthings/smartweather-station-controller.src/smartweather-station-controller.groovy new file mode 100644 index 00000000000..3a40277e41a --- /dev/null +++ b/smartapps/smartthings/smartweather-station-controller.src/smartweather-station-controller.groovy @@ -0,0 +1,67 @@ +/** + * Weather Station Controller + * + * Copyright 2014 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. + * + */ + +definition( + name: "SmartWeather Station Controller", + namespace: "smartthings", + author: "SmartThings", + description: "Updates SmartWeather Station Tile devices every hour.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-MindYourHome.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-MindYourHome@2x.png" +) + +preferences { + section { + input "weatherDevices", "device.smartweatherStationTile" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unschedule() + initialize() +} + +def initialize() { + scheduledEvent() +} + +def scheduledEvent() { + log.info "SmartWeather Station Controller / scheduledEvent terminated due to deprecation" // device handles this itself now -- Bob +/* + log.trace "scheduledEvent()" + + def delayTimeSecs = 60 * 60 // reschedule every 60 minutes + def runAgainWindowMS = 58 * 60 * 1000 // can run at most every 58 minutes + def timeSinceLastRunMS = state.lastRunTime ? now() - state.lastRunTime : null //how long since it last ran? + + if(!timeSinceLastRunMS || timeSinceLastRunMS > runAgainWindowMS){ + runIn(delayTimeSecs, scheduledEvent, [overwrite: false]) + state.lastRunTime = now() + weatherDevices.refresh() + } else { + log.trace "Trying to run smartweather-station-controller too soon. Has only been ${timeSinceLastRunMS} ms but needs to be at least ${runAgainWindowMS} ms" + } + */ +} diff --git a/smartapps/smartthings/sonos-control.src/sonos-control.groovy b/smartapps/smartthings/sonos-control.src/sonos-control.groovy new file mode 100644 index 00000000000..0c93665c37a --- /dev/null +++ b/smartapps/smartthings/sonos-control.src/sonos-control.groovy @@ -0,0 +1,317 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sonos Control + * + * Author: SmartThings + * + * Date: 2013-12-10 + */ +definition( + name: "Sonos Control", + namespace: "smartthings", + author: "SmartThings", + description: "Play or pause your Sonos when certain actions take place in your home.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" +) + +preferences { + page(name: "mainPage", title: "Control your Sonos when something happens", install: true, uninstall: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("When..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section("Perform this action"){ + input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [ + "Play", + "Stop Playing", + "Toggle Play/Pause", + "Skip to Next Track", + "Play Previous Track" + ] + } + section { + input "sonos", "capability.musicPlayer", title: "Sonos music player", required: true + } + section("More options", hideable: true, hidden: true) { + input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (allOk) { + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + log.debug "takeAction($actionType)" + def options = [:] + if (volume) { + sonos.setLevel(volume as Integer) + options.delay = 1000 + } + + switch (actionType) { + case "Play": + options ? sonos.on(options) : sonos.on() + break + case "Stop Playing": + options ? sonos.off(options) : sonos.off() + break + case "Toggle Play/Pause": + def currentStatus = sonos.currentValue("status") + if (currentStatus == "playing") { + options ? sonos.pause(options) : sonos.pause() + } + else { + options ? sonos.play(options) : sonos.play() + } + break + case "Skip to Next Track": + options ? sonos.nextTrack(options) : sonos.nextTrack() + break + case "Play Previous Track": + options ? sonos.previousTrack(options) : sonos.previousTrack() + break + default: + log.error "Action type '$actionType' not defined" + } + + if (frequency) { + state.lastActionTimeStamp = now() + } +} + +private frequencyKey(evt) { + //evt.deviceId ?: evt.value + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + diff --git a/smartapps/smartthings/sonos-mood-music.src/sonos-mood-music.groovy b/smartapps/smartthings/sonos-mood-music.src/sonos-mood-music.groovy new file mode 100644 index 00000000000..8d013c21b14 --- /dev/null +++ b/smartapps/smartthings/sonos-mood-music.src/sonos-mood-music.groovy @@ -0,0 +1,339 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sonos Mood Music + * + * Author: SmartThings + * Date: 2014-02-12 + */ + + +private songOptions() { + + // Make sure current selection is in the set + + def options = new LinkedHashSet() + if (state.selectedSong?.station) { + options << state.selectedSong.station + } + else if (state.selectedSong?.description) { + // TODO - Remove eventually? 'description' for backward compatibility + options << state.selectedSong.description + } + + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List +} + +private saveSelectedSong() { + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + state.selectedSong = data + log.debug "Selected song = $state.selectedSong" + } + else if (song == state.selectedSong?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } +} + +definition( + name: "Sonos Mood Music", + namespace: "smartthings", + author: "SmartThings", + description: "Plays a selected song or station.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" +) + +preferences { + page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true) + page(name: "chooseTrack", title: "Select a song", install: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Play music when..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + + def hideable = anythingSet || app.installationState == "COMPLETE" + def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..." + + section(sectionTitle, hideable: hideable, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section { + input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true + } + section("More options", hideable: true, hidden: true) { + input "volume", "number", title: "Set the volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section{ + input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions() + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + saveSelectedSong() + + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (allOk) { + if (frequency) { + def lastTime = state[frequencyKey(evt)] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + } + else { + takeAction(evt) + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + + log.info "Playing '$state.selectedSong" + + if (volume != null) { + sonos.stop() + pause(500) + sonos.setLevel(volume) + pause(500) + } + + sonos.playTrack(state.selectedSong) + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } + log.trace "Exiting takeAction()" +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + diff --git a/smartapps/smartthings/sonos-music-modes.src/sonos-music-modes.groovy b/smartapps/smartthings/sonos-music-modes.src/sonos-music-modes.groovy new file mode 100644 index 00000000000..29591dbb9b5 --- /dev/null +++ b/smartapps/smartthings/sonos-music-modes.src/sonos-music-modes.groovy @@ -0,0 +1,269 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sonos Mood Music + * + * Author: SmartThings + * Date: 2014-02-12 + */ + + +private songOptions() { + + // Make sure current selection is in the set + + def options = new LinkedHashSet() + options << "STOP PLAYING" + if (state.selectedSong?.station) { + options << state.selectedSong.station + } + else if (state.selectedSong?.description) { + // TODO - Remove eventually? 'description' for backward compatibility + options << state.selectedSong.description + } + + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List +} + +private saveSelectedSongs() { + try { + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + if (!state.selectedSongs) { + state.selectedSongs = [:] + } + + settings.each {name, thisSong -> + if (thisSong == "STOP PLAYING") { + state.selectedSongs."$name" = "PAUSE" + } + if (name.startsWith("mode_")) { + log.info "Looking for $thisSong" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + state.selectedSongs."$name" = data + log.debug "Selected song = $data.station" + } + else if (song == state.selectedSongs."$name"?.station) { + log.debug "Selected existing entry '$thisSong', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$thisSong' not found" + } + } + } + } + catch (Throwable t) { + log.error t + } +} + +definition( + name: "Sonos Music Modes", + namespace: "smartthings", + author: "SmartThings", + description: "Plays a different selected song or station for each mode.", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" +) + +preferences { + page(name: "mainPage", title: "Play a message on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true) + page(name: "chooseTrack", title: "Select a song", install: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + + section { + input "sonos", "capability.musicPlayer", title: "Sonos player", required: true + } + section("More options", hideable: true, hidden: true) { + input "volume", "number", title: "Set the volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section("Play a different song for each mode in which you want music") { + def options = songOptions() + location.modes.each {mode -> + input "mode_$mode.name", "enum", title: mode.name, options: options, required: false + } + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + saveSelectedSongs() + + subscribe(location, modeChangeHandler) +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler($evt.name: $evt.value)" + if (allOk) { + if (frequency) { + def lastTime = state[frequencyKey(evt)] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + } + else { + takeAction(evt) + } + } +} + +private takeAction(evt) { + + def name = "mode_$evt.value".toString() + def selectedSong = state.selectedSongs."$name" + + if (selectedSong == "PAUSE") { + sonos.stop() + } + else { + log.info "Playing '$selectedSong" + + if (volume != null) { + sonos.stop() + pause(500) + sonos.setLevel(volume) + pause(500) + } + + sonos.playTrack(selectedSong) + } + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } + log.trace "Exiting takeAction()" +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + diff --git a/smartapps/smartthings/sonos-notify-with-sound.src/sonos-notify-with-sound.groovy b/smartapps/smartthings/sonos-notify-with-sound.src/sonos-notify-with-sound.groovy new file mode 100644 index 00000000000..0e4176cd1cb --- /dev/null +++ b/smartapps/smartthings/sonos-notify-with-sound.src/sonos-notify-with-sound.groovy @@ -0,0 +1,420 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sonos Custom Message + * + * Author: SmartThings + * Date: 2014-1-29 + */ +definition( + name: "Sonos Notify with Sound", + namespace: "smartthings", + author: "SmartThings", + description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" +) + +preferences { + page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true) + page(name: "chooseTrack", title: "Select a song or station") + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Play message when"){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + def hideable = anythingSet || app.installationState == "COMPLETE" + def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..." + + section(sectionTitle, hideable: hideable, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section{ + input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [ + "Custom Message", + "Bell 1", + "Bell 2", + "Dogs Barking", + "Fire Alarm", + "The mail has arrived", + "A door opened", + "There is motion", + "Smartthings detected a flood", + "Smartthings detected smoke", + "Someone is arriving", + "Piano", + "Lightsaber"] + input "message","text",title:"Play this message", required:false, multiple: false + } + section { + input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true + } + section("More options", hideable: true, hidden: true) { + input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true + href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete" + + input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)", required: false + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section{ + input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions() + } + } +} + +private songOptions() { + + // Make sure current selection is in the set + + def options = new LinkedHashSet() + if (state.selectedSong?.station) { + options << state.selectedSong.station + } + else if (state.selectedSong?.description) { + // TODO - Remove eventually? 'description' for backward compatibility + options << state.selectedSong.description + } + + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List +} + +private saveSelectedSong() { + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + state.selectedSong = data + log.debug "Selected song = $state.selectedSong" + } + else if (song == state.selectedSong?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } + + if (song) { + saveSelectedSong() + } + + loadText() +} + +def eventHandler(evt) { + log.trace "eventHandler($evt?.name: $evt?.value)" + if (allOk) { + log.trace "allOk" + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + + log.trace "takeAction()" + + if (song) { + sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume) + } + else if (resumePlaying){ + sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume) + } + else { + sonos.playTrackAndRestore(state.sound.uri, state.sound.duration, volume) + } + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } + log.trace "Exiting takeAction()" +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private getTimeLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + +private loadText() { + switch ( actionType) { + case "Bell 1": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] + break; + case "Bell 2": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"] + break; + case "Dogs Barking": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"] + break; + case "Fire Alarm": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"] + break; + case "The mail has arrived": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"] + break; + case "A door opened": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"] + break; + case "There is motion": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"] + break; + case "Smartthings detected a flood": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"] + break; + case "Smartthings detected smoke": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"] + break; + case "Someone is arriving": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"] + break; + case "Piano": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"] + break; + case "Lightsaber": + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"] + break; + default: + if (message) { + state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed) + } + else { + state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App") + } + break; + } +} diff --git a/smartapps/smartthings/sonos-remote-control.src/sonos-remote-control.groovy b/smartapps/smartthings/sonos-remote-control.src/sonos-remote-control.groovy new file mode 100644 index 00000000000..eed02d26ea6 --- /dev/null +++ b/smartapps/smartthings/sonos-remote-control.src/sonos-remote-control.groovy @@ -0,0 +1,165 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sonos Remote Control + * + * Author: Matt Nohr + * Date: 2014-04-14 + */ + +/** + * Buttons: + * 1 2 + * 3 4 + * + * Pushed: + * 1: Play/Pause + * 2: Volume Up + * 3: Next Track + * 4: Volume Down + * + * Held: + * 1: + * 2: Volume Up (2x) + * 3: Previous Track + * 4: Volume Down (2x) + */ + +definition( + name: "Sonos Remote Control", + namespace: "smartthings", + author: "SmartThings", + description: "Control your Sonos system with an Aeon Minimote", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + section("Select your devices") { + input "buttonDevice", "capability.button", title: "Minimote", multiple: false, required: true + input "sonos", "capability.musicPlayer", title: "Sonos", multiple: false, required: true + } + section("Options") { + input "volumeOffset", "number", title: "Adjust Volume by this amount", required: false, description: "optional - 5% default" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(buttonDevice, "button", buttonEvent) +} + +def buttonEvent(evt){ + def buttonNumber = evt.data + def value = evt.value + log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)" + log.debug "button: $buttonNumber, value: $value" + + def recentEvents = buttonDevice.eventsSince(new Date(now() - 2000)).findAll{it.value == evt.value && it.data == evt.data} + log.debug "Found ${recentEvents.size()?:0} events in past 2 seconds" + + if(recentEvents.size <= 1){ + handleButton(extractButtonNumber(buttonNumber), value) + } else { + log.debug "Found recent button press events for $buttonNumber with value $value" + } +} + +def extractButtonNumber(data) { + def buttonNumber + //TODO must be a better way to do this. Data is like {buttonNumber:1} + switch(data) { + case ~/.*1.*/: + buttonNumber = 1 + break + case ~/.*2.*/: + buttonNumber = 2 + break + case ~/.*3.*/: + buttonNumber = 3 + break + case ~/.*4.*/: + buttonNumber = 4 + break + } + return buttonNumber +} + +def handleButton(buttonNumber, value) { + switch([number: buttonNumber, value: value]) { + case{it.number == 1 && it.value == 'pushed'}: + log.debug "Button 1 pushed - Play/Pause" + togglePlayPause() + break + case{it.number == 2 && it.value == 'pushed'}: + log.debug "Button 2 pushed - Volume Up" + adjustVolume(true, false) + break + case{it.number == 3 && it.value == 'pushed'}: + log.debug "Button 3 pushed - Next Track" + sonos.nextTrack() + break + case{it.number == 4 && it.value == 'pushed'}: + log.debug "Button 4 pushed - Volume Down" + adjustVolume(false, false) + break + case{it.number == 2 && it.value == 'held'}: + log.debug "Button 2 held - Volume Up 2x" + adjustVolume(true, true) + break + case{it.number == 3 && it.value == 'held'}: + log.debug "Button 3 held - Previous Track" + sonos.previousTrack() + break + case{it.number == 4 && it.value == 'held'}: + log.debug "Button 4 held - Volume Down 2x" + adjustVolume(false, true) + break + default: + log.debug "Unhandled command: $buttonNumber $value" + + } +} + +def togglePlayPause() { + def currentStatus = sonos.currentValue("status") + if (currentStatus == "playing") { + options ? sonos.pause(options) : sonos.pause() + } + else { + options ? sonos.play(options) : sonos.play() + } +} + +def adjustVolume(boolean up, boolean doubleAmount) { + def changeAmount = (volumeOffset ?: 5) * (doubleAmount ? 2 : 1) + def currentVolume = sonos.currentValue("level") + + if(up) { + sonos.setLevel(currentVolume + changeAmount) + } else { + sonos.setLevel(currentVolume - changeAmount) + } +} diff --git a/smartapps/smartthings/sonos-weather-forecast.src/sonos-weather-forecast.groovy b/smartapps/smartthings/sonos-weather-forecast.src/sonos-weather-forecast.groovy new file mode 100644 index 00000000000..a730729e782 --- /dev/null +++ b/smartapps/smartthings/sonos-weather-forecast.src/sonos-weather-forecast.groovy @@ -0,0 +1,432 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sonos Weather Forecast + * + * Author: SmartThings + * Date: 2014-1-29 + */ +definition( + name: "Sonos Weather Forecast", + namespace: "smartthings", + author: "SmartThings", + description: "Play a weather report through your Sonos when the mode changes or other events occur", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" +) + +preferences { + page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true) + page(name: "chooseTrack", title: "Select a song or station") + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("Play weather report when"){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + def hideable = anythingSet || app.installationState == "COMPLETE" + def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..." + + section(sectionTitle, hideable: hideable, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section { + input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true, + options: [ + ["0": "Current Conditions"], + ["1": "Today's Forecast"], + ["2": "Tonight's Forecast"], + ["3": "Tomorrow's Forecast"], + ] + ) + } + section { + input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true + } + section("More options", hideable: true, hidden: true) { + input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true + href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete" + + input "zipCode", "text", title: "Zip Code", required: false + input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section{ + input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions() + } + } +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location,modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } + + if (song) { + saveSelectedSong() + } +} + +def eventHandler(evt) { + log.trace "eventHandler($evt?.name: $evt?.value)" + if (allOk) { + log.trace "allOk" + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + + loadText() + + if (song) { + sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume) + } + else if (resumePlaying){ + sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume) + } + else if (volume) { + sonos.playTrackAtVolume(state.sound.uri, volume) + } + else { + sonos.playTrack(state.sound.uri) + } + + if (frequency || oncePerDay) { + state[frequencyKey(evt)] = now() + } +} + +private songOptions() { + + // Make sure current selection is in the set + + def options = new LinkedHashSet() + if (state.selectedSong?.station) { + options << state.selectedSong.station + } + else if (state.selectedSong?.description) { + // TODO - Remove eventually? 'description' for backward compatibility + options << state.selectedSong.description + } + + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List +} + +private saveSelectedSong() { + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + state.selectedSong = data + log.debug "Selected song = $state.selectedSong" + } + else if (song == state.selectedSong?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } +} + +private frequencyKey(evt) { + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private getTimeLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize + +private loadText() { + if (location.timeZone || zipCode) { + def weather = getWeatherFeature("forecast", zipCode) + def current = getWeatherFeature("conditions", zipCode) + def isMetric = location.temperatureScale == "C" + def delim = "" + def sb = new StringBuilder() + list(forecastOptions).sort().each {opt -> + if (opt == "0") { + if (isMetric) { + sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees." + } + else { + sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees." + } + delim = " " + } + else if (opt == "1") { + sb << delim + sb << "Today's forecast is " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[0].fcttext + } + } + else if (opt == "2") { + sb << delim + sb << "Tonight will be " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[1].fcttext + } + } + else if (opt == "3") { + sb << delim + sb << "Tomorrow will be " + if (isMetric) { + sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric + } + else { + sb << weather.forecast.txt_forecast.forecastday[2].fcttext + } + } + } + + def msg = sb.toString() + msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') // TODO - remove after next release + log.debug "msg = ${msg}" + state.sound = textToSpeech(msg, true) + } + else { + state.sound = textToSpeech("Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.") + } +} + +private list(String s) { + [s] +} +private list(l) { + l +} diff --git a/smartapps/smartthings/step-notifier.src/step-notifier.groovy b/smartapps/smartthings/step-notifier.src/step-notifier.groovy new file mode 100644 index 00000000000..7aec19c1782 --- /dev/null +++ b/smartapps/smartthings/step-notifier.src/step-notifier.groovy @@ -0,0 +1,366 @@ +/** + * Step Notifier + * + * Copyright 2014 Jeff's Account + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Step Notifier", + namespace: "smartthings", + author: "SmartThings", + description: "Use a step tracker device to track daily step goals and trigger various device actions when your goals are met!", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png" +) + +preferences { + page(name: "setupNotifications") + page(name: "chooseTrack", title: "Select a song or station") + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def setupNotifications() { + + dynamicPage(name: "setupNotifications", title: "Configure Your Goal Notifications.", install: true, uninstall: true) { + + section("Select your Jawbone UP") { + input "jawbone", "device.jawboneUser", title: "Jawbone UP", required: true, multiple: false + } + + section("Notify Me When"){ + input "thresholdType", "enum", title: "Select When to Notify", required: false, defaultValue: "Goal Reached", options: [["Goal":"Goal Reached"],["Threshold":"Specific Number of Steps"]], submitOnChange:true + if (settings.thresholdType) { + if (settings.thresholdType == "Threshold") { + input "threshold", "number", title: "Enter Step Threshold", description: "Number", required: true + } + } + } + + section("Via a push notification and/or an SMS message"){ + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + input "notificationType", "enum", title: "Select Notification", required: false, defaultValue: "None", options: ["None", "Push", "SMS", "Both"] + } + } + + section("Flash the Lights") { + input "lights", "capability.switch", title: "Which Lights?", required: false, multiple: true + input "flashCount", "number", title: "How Many Times?", defaultValue: 5, required: false + } + + section("Change the Color of the Lights") { + input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required:false, multiple:true + input "color", "enum", title: "Hue Color?", required: false, multiple:false, options: ["Red","Green","Blue","Yellow","Orange","Purple","Pink"] + input "lightLevel", "enum", title: "Light Level?", required: false, options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]] + input "duration", "number", title: "Duration in Seconds?", defaultValue: 30, required: false + } + + section("Play a song on the Sonos") { + input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: false, submitOnChange:true + if (settings.sonos) { + input "song","enum",title:"Play this track or radio station", required:true, multiple: false, options: songOptions() + input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true + input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false + input "songDuration", "number", title: "Play for this many seconds", defaultValue: 60, description: "0-100%", required: true + } + + } + } +} + +def chooseTrack() { + dynamicPage(name: "chooseTrack") { + section{ + input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions() + } + } +} + +private songOptions() { + + // Make sure current selection is in the set + + def options = new LinkedHashSet() + if (state.selectedSong?.station) { + options << state.selectedSong.station + } + else if (state.selectedSong?.description) { + // TODO - Remove eventually? 'description' for backward compatibility + options << state.selectedSong.description + } + + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List +} + +private saveSelectedSong() { + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + state.selectedSong = data + log.debug "Selected song = $state.selectedSong" + } + else if (song == state.selectedSong?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + + log.trace "Entering initialize()" + + state.lastSteps = 0 + state.steps = jawbone.currentValue("steps").toInteger() + state.goal = jawbone.currentValue("goal").toInteger() + + subscribe (jawbone,"goal",goalHandler) + subscribe (jawbone,"steps",stepHandler) + + if (song) { + saveSelectedSong() + } + + log.trace "Exiting initialize()" +} + +def goalHandler(evt) { + + log.trace "Entering goalHandler()" + + def goal = evt.value.toInteger() + + state.goal = goal + + log.trace "Exiting goalHandler()" +} + +def stepHandler(evt) { + + log.trace "Entering stepHandler()" + + log.debug "Event Value ${evt.value}" + log.debug "state.steps = ${state.steps}" + log.debug "state.goal = ${state.goal}" + + def steps = evt.value.toInteger() + + state.lastSteps = state.steps + state.steps = steps + + def stepGoal + if (settings.thresholdType == "Goal") + stepGoal = state.goal + else + stepGoal = settings.threshold + + if ((state.lastSteps < stepGoal) && (state.steps >= stepGoal)) { // only trigger when crossing through the goal threshold + + // goal achieved for the day! Yay! Lets tell someone! + + if (settings.notificationType != "None") { // Push or SMS Notification requested + + if (location.contactBookEnabled) { + sendNotificationToContacts(stepMessage, recipients) + } + else { + + def options = [ + method: settings.notificationType.toLowerCase(), + phone: settings.phone + ] + + sendNotification(stepMessage, options) + } + } + + if (settings.sonos) { // play a song on the Sonos as requested + + // runIn(1, sonosNotification, [overwrite: false]) + sonosNotification() + + } + + if (settings.hues) { // change the color of hue bulbs ras equested + + // runIn(1, hueNotification, [overwrite: false]) + hueNotification() + + } + + if (settings.lights) { // flash the lights as requested + + // runIn(1, lightsNotification, [overwrite: false]) + lightsNotification() + + } + + } + + log.trace "Exiting stepHandler()" + +} + + +def lightsNotification() { + + // save the current state of the lights + + log.trace "Save current state of lights" + + state.previousLights = [:] + + lights.each { + state.previousLights[it.id] = it.currentValue("switch") + } + + // Flash the light on and off 5 times for now - this could be configurable + + log.trace "Now flash the lights" + + for (i in 1..flashCount) { + + lights.on() + pause(500) + lights.off() + + } + + // restore the original state + + log.trace "Now restore the original state of lights" + + lights.each { + it."${state.previousLights[it.id]}"() + } + + +} + +def hueNotification() { + + log.trace "Entering hueNotification()" + + def hueColor = 0 + if(color == "Blue") + hueColor = 70//60 + else if(color == "Green") + hueColor = 39//30 + else if(color == "Yellow") + hueColor = 25//16 + else if(color == "Orange") + hueColor = 10 + else if(color == "Purple") + hueColor = 75 + else if(color == "Pink") + hueColor = 83 + + + state.previousHue = [:] + + hues.each { + state.previousHue[it.id] = [ + "switch": it.currentValue("switch"), + "level" : it.currentValue("level"), + "hue": it.currentValue("hue"), + "saturation": it.currentValue("saturation") + ] + } + + log.debug "current values = ${state.previousHue}" + + def newValue = [hue: hueColor, saturation: 100, level: (lightLevel as Integer) ?: 100] + log.debug "new value = $newValue" + + hues*.setColor(newValue) + setTimer() + + log.trace "Exiting hueNotification()" + +} + +def setTimer() +{ + log.debug "runIn ${duration}, resetHue" + runIn(duration, resetHue, [overwrite: false]) +} + + +def resetHue() +{ + log.trace "Entering resetHue()" + settings.hues.each { + it.setColor(state.previousHue[it.id]) + } + log.trace "Exiting resetHue()" +} + +def sonosNotification() { + + log.trace "sonosNotification()" + + if (settings.song) { + + if (settings.resumePlaying) { + if (settings.volume) + sonos.playTrackAndResume(state.selectedSong, settings.songDuration, settings.volume) + else + sonos.playTrackAndResume(state.selectedSong, settings.songDuration) + } else { + if (settings.volume) + sonos.playTrackAtVolume(state.selectedSong, settings.volume) + else + sonos.playTrack(state.selectedSong) + } + + sonos.on() // make sure it is playing + + } + + log.trace "Exiting sonosNotification()" +} diff --git a/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy b/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy new file mode 100644 index 00000000000..417cce6367c --- /dev/null +++ b/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy @@ -0,0 +1,189 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Sunrise, Sunset + * + * Author: SmartThings + * + * Date: 2013-04-30 + */ +definition( + name: "Sunrise/Sunset", + namespace: "smartthings", + author: "SmartThings", + description: "Changes mode and controls lights based on local sunrise and sunset times.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine@2x.png" +) + +preferences { + section ("At sunrise...") { + input "sunriseMode", "mode", title: "Change mode to?", required: false + input "sunriseOn", "capability.switch", title: "Turn on?", required: false, multiple: true + input "sunriseOff", "capability.switch", title: "Turn off?", required: false, multiple: true + } + section ("At sunset...") { + input "sunsetMode", "mode", title: "Change mode to?", required: false + input "sunsetOn", "capability.switch", title: "Turn on?", required: false, multiple: true + input "sunsetOff", "capability.switch", title: "Turn off?", required: false, multiple: true + } + section ("Sunrise offset (optional)...") { + input "sunriseOffsetValue", "text", title: "HH:MM", required: false + input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] + } + section ("Sunset offset (optional)...") { + input "sunsetOffsetValue", "text", title: "HH:MM", required: false + input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] + } + section ("Zip code (optional, defaults to location coordinates)...") { + input "zipCode", "text", required: false + } + section( "Notifications" ) { + input("recipients", "contact", title: "Send notifications to") { + input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false + input "phoneNumber", "phone", title: "Send a text message?", required: false + } + } + +} + +def installed() { + initialize() +} + +def updated() { + unsubscribe() + //unschedule handled in astroCheck method + initialize() +} + +def initialize() { + subscribe(location, "position", locationPositionChange) + subscribe(location, "sunriseTime", sunriseSunsetTimeHandler) + subscribe(location, "sunsetTime", sunriseSunsetTimeHandler) + + astroCheck() +} + +def locationPositionChange(evt) { + log.trace "locationChange()" + astroCheck() +} + +def sunriseSunsetTimeHandler(evt) { + log.trace "sunriseSunsetTimeHandler()" + astroCheck() +} + +def astroCheck() { + def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) + + def now = new Date() + def riseTime = s.sunrise + def setTime = s.sunset + log.debug "riseTime: $riseTime" + log.debug "setTime: $setTime" + + if (state.riseTime != riseTime.time) { + unschedule("sunriseHandler") + + if(riseTime.before(now)) { + riseTime = riseTime.next() + } + + state.riseTime = riseTime.time + + log.info "scheduling sunrise handler for $riseTime" + schedule(riseTime, sunriseHandler) + } + + if (state.setTime != setTime.time) { + unschedule("sunsetHandler") + + if(setTime.before(now)) { + setTime = setTime.next() + } + + state.setTime = setTime.time + + log.info "scheduling sunset handler for $setTime" + schedule(setTime, sunsetHandler) + } +} + +def sunriseHandler() { + log.info "Executing sunrise handler" + if (sunriseOn) { + sunriseOn.on() + } + if (sunriseOff) { + sunriseOff.off() + } + changeMode(sunriseMode) +} + +def sunsetHandler() { + log.info "Executing sunset handler" + if (sunsetOn) { + sunsetOn.on() + } + if (sunsetOff) { + sunsetOff.off() + } + changeMode(sunsetMode) +} + +def changeMode(newMode) { + if (newMode && location.mode != newMode) { + if (location.modes?.find{it.name == newMode}) { + setLocationMode(newMode) + send "${label} has changed the mode to '${newMode}'" + } + else { + send "${label} tried to change to undefined mode '${newMode}'" + } + } +} + +private send(msg) { + if (location.contactBookEnabled) { + log.debug("sending notifications to: ${recipients?.size()}") + sendNotificationToContacts(msg, recipients) + } + else { + if (sendPushMessage != "No") { + log.debug("sending push message") + sendPush(msg) + } + + if (phoneNumber) { + log.debug("sending text message") + sendSms(phoneNumber, msg) + } + } + + log.debug msg +} + +private getLabel() { + app.label ?: "SmartThings" +} + +private getSunriseOffset() { + sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null +} + +private getSunsetOffset() { + sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null +} + diff --git a/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy b/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy new file mode 100644 index 00000000000..87b95866cd0 --- /dev/null +++ b/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy @@ -0,0 +1,419 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Tesla Service Manager + * + * Author: juano23@gmail.com + * Date: 2013-08-15 + */ + +definition( + name: "Tesla (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Integrate your Tesla car with SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png" +) + +preferences { + page(name: "loginToTesla", title: "Tesla") + page(name: "selectCars", title: "Tesla") +} + +def loginToTesla() { + def showUninstall = username != null && password != null + return dynamicPage(name: "loginToTesla", title: "Connect your Tesla", nextPage:"selectCars", uninstall:showUninstall) { + section("Log in to your Tesla account:") { + input "username", "text", title: "Username", required: true, autoCorrect:false + input "password", "password", title: "Password", required: true, autoCorrect:false + } + section("To use Tesla, SmartThings encrypts and securely stores your Tesla credentials.") {} + } +} + +def selectCars() { + def loginResult = forceLogin() + + if(loginResult.success) + { + def options = carsDiscovered() ?: [] + + return dynamicPage(name: "selectCars", title: "Tesla", install:true, uninstall:true) { + section("Select which Tesla to connect"){ + input(name: "selectedCars", type: "enum", required:false, multiple:true, options:options) + } + } + } + else + { + log.error "login result false" + return dynamicPage(name: "selectCars", title: "Tesla", install:false, uninstall:true, nextPage:"") { + section("") { + paragraph "Please check your username and password" + } + } + } +} + + +def installed() { + log.debug "Installed" + initialize() +} + +def updated() { + log.debug "Updated" + + unsubscribe() + initialize() +} + +def uninstalled() { + removeChildDevices(getChildDevices()) +} + +def initialize() { + + if (selectCars) { + addDevice() + } + + // Delete any that are no longer in settings + def delete = getChildDevices().findAll { !selectedCars } + log.info delete + //removeChildDevices(delete) +} + +//CHILD DEVICE METHODS +def addDevice() { + def devices = getcarList() + log.trace "Adding childs $devices - $selectedCars" + selectedCars.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newCar = devices.find { (it.dni) == dni } + d = addChildDevice("smartthings", "Tesla", dni, null, [name:"Tesla", label:"Tesla"]) + log.trace "created ${d.name} with id $dni" + } else { + log.trace "found ${d.name} with id $key already exists" + } + } +} + +private removeChildDevices(delete) +{ + log.debug "deleting ${delete.size()} Teslas" + delete.each { + state.suppressDelete[it.deviceNetworkId] = true + deleteChildDevice(it.deviceNetworkId) + state.suppressDelete.remove(it.deviceNetworkId) + } +} + +def getcarList() { + def devices = [] + + def carListParams = [ + uri: "https://portal.vn.teslamotors.com/", + path: "/vehicles", + headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()] + ] + + httpGet(carListParams) { resp -> + log.debug "Getting car list" + if(resp.status == 200) { + def vehicleId = resp.data.id.value[0].toString() + def vehicleVIN = resp.data.vin[0] + def dni = vehicleVIN + ":" + vehicleId + def name = "Tesla [${vehicleId}]" + // CHECK HERE IF MOBILE IS ENABLE + // path: "/vehicles/${vehicleId}/mobile_enabled", + // if (enable) + devices += ["name" : "${name}", "dni" : "${dni}"] + // else return [errorMessage:"Mobile communication isn't enable on all of your vehicles."] + } else if(resp.status == 302) { + // Token expired or incorrect + singleUrl = resp.headers.Location.value + } else { + // ERROR + log.error "car list: unknown response" + } + } + return devices +} + +Map carsDiscovered() { + def devices = getcarList() + log.trace "Map $devices" + def map = [:] + if (devices instanceof java.util.Map) { + devices.each { + def value = "${it?.name}" + def key = it?.dni + map["${key}"] = value + } + } else { //backwards compatable + devices.each { + def value = "${it?.name}" + def key = it?.dni + map["${key}"] = value + } + } + map +} + +def removeChildFromSettings(child) { + def device = child.device + def dni = device.deviceNetworkId + log.debug "removing child device $device with dni ${dni}" + if(!state?.suppressDelete?.get(dni)) + { + def newSettings = settings.cars?.findAll { it != dni } ?: [] + app.updateSetting("cars", newSettings) + } +} + +private forceLogin() { + updateCookie(null) + login() +} + + +private login() { + if(getCookieValueIsValid()) { + return [success:true] + } + return doLogin() +} + +private doLogin() { + def loginParams = [ + uri: "https://portal.vn.teslamotors.com", + path: "/login", + contentType: "application/x-www-form-urlencoded", + body: "user_session%5Bemail%5D=${username}&user_session%5Bpassword%5D=${password}" + ] + + def result = [success:false] + + try { + httpPost(loginParams) { resp -> + if (resp.status == 302) { + log.debug "login 302 json headers: " + resp.headers.collect { "${it.name}:${it.value}" } + def cookie = resp?.headers?.'Set-Cookie'?.split(";")?.getAt(0) + if (cookie) { + log.debug "login setting cookie to $cookie" + updateCookie(cookie) + result.success = true + } else { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + } else { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + } + } catch (groovyx.net.http.HttpResponseException e) { + result.reason = "Bad login" + } + return result +} + +private command(String dni, String command, String value = '') { + def id = getVehicleId(dni) + def commandPath + switch (command) { + case "flash": + commandPath = "/vehicles/${id}/command/flash_lights" + break; + case "honk": + commandPath = "/vehicles/${id}/command/honk_horn" + break; + case "doorlock": + commandPath = "/vehicles/${id}/command/door_lock" + break; + case "doorunlock": + commandPath = "/vehicles/${id}/command/door_unlock" + break; + case "climaon": + commandPath = "/vehicles/${id}/command/auto_conditioning_start" + break; + case "climaoff": + commandPath = "/vehicles/${id}/command/auto_conditioning_stop" + break; + case "roof": + commandPath = "/vehicles/${id}/command/sun_roof_control?state=${value}" + break; + case "temp": + commandPath = "/vehicles/${id}/command/set_temps?driver_temp=${value}&passenger_temp=${value}" + break; + default: + break; + } + + def commandParams = [ + uri: "https://portal.vn.teslamotors.com", + path: commandPath, + headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()] + ] + + def loginRequired = false + + httpGet(commandParams) { resp -> + + if(resp.status == 403) { + loginRequired = true + } else if (resp.status == 200) { + def data = resp.data + sendNotification(data.toString()) + } else { + log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}" + } + } + if(loginRequired) { throw new Exception("Login Required") } +} + +private honk(String dni) { + def id = getVehicleId(dni) + def honkParams = [ + uri: "https://portal.vn.teslamotors.com", + path: "/vehicles/${id}/command/honk_horn", + headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()] + ] + + def loginRequired = false + + httpGet(honkParams) { resp -> + + if(resp.status == 403) { + loginRequired = true + } else if (resp.status == 200) { + def data = resp.data + } else { + log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}" + } + } + + if(loginRequired) { + throw new Exception("Login Required") + } +} + +private poll(String dni) { + def id = getVehicleId(dni) + def pollParams1 = [ + uri: "https://portal.vn.teslamotors.com", + path: "/vehicles/${id}/command/climate_state", + headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()] + ] + + def childDevice = getChildDevice(dni) + + def loginRequired = false + + httpGet(pollParams1) { resp -> + + if(resp.status == 403) { + loginRequired = true + } else if (resp.status == 200) { + def data = resp.data + childDevice?.sendEvent(name: 'temperature', value: cToF(data.inside_temp).toString()) + if (data.is_auto_conditioning_on) + childDevice?.sendEvent(name: 'clima', value: 'on') + else + childDevice?.sendEvent(name: 'clima', value: 'off') + childDevice?.sendEvent(name: 'thermostatSetpoint', value: cToF(data.driver_temp_setting).toString()) + } else { + log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}" + } + } + + def pollParams2 = [ + uri: "https://portal.vn.teslamotors.com", + path: "/vehicles/${id}/command/vehicle_state", + headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()] + ] + + httpGet(pollParams2) { resp -> + if(resp.status == 403) { + loginRequired = true + } else if (resp.status == 200) { + def data = resp.data + if (data.sun_roof_percent_open == 0) + childDevice?.sendEvent(name: 'roof', value: 'close') + else if (data.sun_roof_percent_open > 0 && data.sun_roof_percent_open < 70) + childDevice?.sendEvent(name: 'roof', value: 'vent') + else if (data.sun_roof_percent_open >= 70 && data.sun_roof_percent_open <= 80) + childDevice?.sendEvent(name: 'roof', value: 'comfort') + else if (data.sun_roof_percent_open > 80 && data.sun_roof_percent_open <= 100) + childDevice?.sendEvent(name: 'roof', value: 'open') + if (data.locked) + childDevice?.sendEvent(name: 'door', value: 'lock') + else + childDevice?.sendEvent(name: 'door', value: 'unlock') + } else { + log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}" + } + } + + def pollParams3 = [ + uri: "https://portal.vn.teslamotors.com", + path: "/vehicles/${id}/command/charge_state", + headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()] + ] + + httpGet(pollParams3) { resp -> + if(resp.status == 403) { + loginRequired = true + } else if (resp.status == 200) { + def data = resp.data + childDevice?.sendEvent(name: 'connected', value: data.charging_state.toString()) + childDevice?.sendEvent(name: 'miles', value: data.battery_range.toString()) + childDevice?.sendEvent(name: 'battery', value: data.battery_level.toString()) + } else { + log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}" + } + } + + if(loginRequired) { + throw new Exception("Login Required") + } +} + +private getVehicleId(String dni) { + return dni.split(":").last() +} + +private Boolean getCookieValueIsValid() +{ + // TODO: make a call with the cookie to verify that it works + return getCookieValue() +} + +private updateCookie(String cookie) { + state.cookie = cookie +} + +private getCookieValue() { + state.cookie +} + +def cToF(temp) { + return temp * 1.8 + 32 +} + +private validUserAgent() { + "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5" +} \ No newline at end of file diff --git a/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy b/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy new file mode 100644 index 00000000000..08a98875c98 --- /dev/null +++ b/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy @@ -0,0 +1,58 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Text Me When It Opens + * + * Author: SmartThings + */ +definition( + name: "Text Me When It Opens", + namespace: "smartthings", + author: "SmartThings", + description: "Get a text message sent to your phone when an open/close sensor is opened.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" +) + +preferences { + section("When the door opens...") { + input "contact1", "capability.contactSensor", title: "Where?" + } + section("Text me at...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + +def installed() +{ + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + log.trace "$evt.value: $evt, $settings" + log.debug "$contact1 was opened, texting $phone1" + if (location.contactBookEnabled) { + sendNotificationToContacts("Your ${contact1.label ?: contact1.name} was opened", recipients) + } + else { + sendSms(phone1, "Your ${contact1.label ?: contact1.name} was opened") + } +} diff --git a/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy b/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy new file mode 100644 index 00000000000..a3fac54c4bd --- /dev/null +++ b/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy @@ -0,0 +1,77 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Text Me When There's Motion and I'm Not Here + * + * Author: SmartThings + */ + +definition( + name: "Text Me When There's Motion and I'm Not Here", + namespace: "smartthings", + author: "SmartThings", + description: "Send a text message when there is motion while you are away.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence@2x.png" +) + +preferences { + section("When there's movement...") { + input "motion1", "capability.motionSensor", title: "Where?" + } + section("While I'm out...") { + input "presence1", "capability.presenceSensor", title: "Who?" + } + section("Text me at...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + +def installed() { + subscribe(motion1, "motion.active", motionActiveHandler) +} + +def updated() { + unsubscribe() + subscribe(motion1, "motion.active", motionActiveHandler) +} + +def motionActiveHandler(evt) { + log.trace "$evt.value: $evt, $settings" + + if (presence1.latestValue("presence") == "not present") { + // Don't send a continuous stream of text messages + def deltaSeconds = 10 + def timeAgo = new Date(now() - (1000 * deltaSeconds)) + def recentEvents = motion1.eventsSince(timeAgo) + log.debug "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds" + def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 + + if (alreadySentSms) { + log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + } else { + if (location.contactBookEnabled) { + log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}" + sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients) + } + else { + log.debug "$motion1 has moved while you were out, texting $phone1" + sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out") + } + } + } else { + log.debug "Motion detected, but presence sensor indicates you are present" + } +} diff --git a/smartapps/smartthings/the-big-switch.src/the-big-switch.groovy b/smartapps/smartthings/the-big-switch.src/the-big-switch.groovy new file mode 100644 index 00000000000..e631fa013ee --- /dev/null +++ b/smartapps/smartthings/the-big-switch.src/the-big-switch.groovy @@ -0,0 +1,93 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * The Big Switch + * + * Author: SmartThings + * + * Date: 2013-05-01 + */ +definition( + name: "The Big Switch", + namespace: "smartthings", + author: "SmartThings", + description: "Turns on, off and dim a collection of lights based on the state of a specific switch.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" +) + +preferences { + section("When this switch is turned on, off or dimmed") { + input "master", "capability.switch", title: "Where?" + } + section("Turn on or off all of these switches as well") { + input "switches", "capability.switch", multiple: true, required: false + } + section("And turn off but not on all of these switches") { + input "offSwitches", "capability.switch", multiple: true, required: false + } + section("And turn on but not off all of these switches") { + input "onSwitches", "capability.switch", multiple: true, required: false + } + section("And Dim these switches") { + input "dimSwitches", "capability.switchLevel", multiple: true, required: false + } +} + +def installed() +{ + subscribe(master, "switch.on", onHandler) + subscribe(master, "switch.off", offHandler) + subscribe(master, "level", dimHandler) +} + +def updated() +{ + unsubscribe() + subscribe(master, "switch.on", onHandler) + subscribe(master, "switch.off", offHandler) + subscribe(master, "level", dimHandler) +} + +def logHandler(evt) { + log.debug evt.value +} + +def onHandler(evt) { + log.debug evt.value + log.debug onSwitches() + onSwitches()?.on() +} + +def offHandler(evt) { + log.debug evt.value + log.debug offSwitches() + offSwitches()?.off() +} + +def dimHandler(evt) { + log.debug "Dim level: $evt.value" + dimSwitches?.setLevel(evt.value) +} + +private onSwitches() { + if(switches && onSwitches) { switches + onSwitches } + else if(switches) { switches } + else { onSwitches } +} + +private offSwitches() { + if(switches && offSwitches) { switches + offSwitches } + else if(switches) { switches } + else { offSwitches } +} diff --git a/smartapps/smartthings/the-flasher.src/the-flasher.groovy b/smartapps/smartthings/the-flasher.src/the-flasher.groovy new file mode 100644 index 00000000000..60feb4ceace --- /dev/null +++ b/smartapps/smartthings/the-flasher.src/the-flasher.groovy @@ -0,0 +1,150 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * The Flasher + * + * Author: bob + * Date: 2013-02-06 + */ +definition( + name: "The Flasher", + namespace: "smartthings", + author: "SmartThings", + description: "Flashes a set of lights in response to motion, an open/close event, or a switch.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-contact@2x.png" +) + +preferences { + section("When any of the following devices trigger..."){ + input "motion", "capability.motionSensor", title: "Motion Sensor?", required: false + input "contact", "capability.contactSensor", title: "Contact Sensor?", required: false + input "acceleration", "capability.accelerationSensor", title: "Acceleration Sensor?", required: false + input "mySwitch", "capability.switch", title: "Switch?", required: false + input "myPresence", "capability.presenceSensor", title: "Presence Sensor?", required: false + } + section("Then flash..."){ + input "switches", "capability.switch", title: "These lights", multiple: true + input "numFlashes", "number", title: "This number of times (default 3)", required: false + } + section("Time settings in milliseconds (optional)..."){ + input "onFor", "number", title: "On for (default 1000)", required: false + input "offFor", "number", title: "Off for (default 1000)", required: false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + subscribe() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + subscribe() +} + +def subscribe() { + if (contact) { + subscribe(contact, "contact.open", contactOpenHandler) + } + if (acceleration) { + subscribe(acceleration, "acceleration.active", accelerationActiveHandler) + } + if (motion) { + subscribe(motion, "motion.active", motionActiveHandler) + } + if (mySwitch) { + subscribe(mySwitch, "switch.on", switchOnHandler) + } + if (myPresence) { + subscribe(myPresence, "presence", presenceHandler) + } +} + +def motionActiveHandler(evt) { + log.debug "motion $evt.value" + flashLights() +} + +def contactOpenHandler(evt) { + log.debug "contact $evt.value" + flashLights() +} + +def accelerationActiveHandler(evt) { + log.debug "acceleration $evt.value" + flashLights() +} + +def switchOnHandler(evt) { + log.debug "switch $evt.value" + flashLights() +} + +def presenceHandler(evt) { + log.debug "presence $evt.value" + if (evt.value == "present") { + flashLights() + } else if (evt.value == "not present") { + flashLights() + } +} + +private flashLights() { + def doFlash = true + def onFor = onFor ?: 1000 + def offFor = offFor ?: 1000 + def numFlashes = numFlashes ?: 3 + + log.debug "LAST ACTIVATED IS: ${state.lastActivated}" + if (state.lastActivated) { + def elapsed = now() - state.lastActivated + def sequenceTime = (numFlashes + 1) * (onFor + offFor) + doFlash = elapsed > sequenceTime + log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}" + } + + if (doFlash) { + log.debug "FLASHING $numFlashes times" + state.lastActivated = now() + log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}" + def initialActionOn = switches.collect{it.currentSwitch != "on"} + def delay = 0L + numFlashes.times { + log.trace "Switch on after $delay msec" + switches.eachWithIndex {s, i -> + if (initialActionOn[i]) { + s.on(delay: delay) + } + else { + s.off(delay:delay) + } + } + delay += onFor + log.trace "Switch off after $delay msec" + switches.eachWithIndex {s, i -> + if (initialActionOn[i]) { + s.off(delay: delay) + } + else { + s.on(delay:delay) + } + } + delay += offFor + } + } +} + diff --git a/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy b/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy new file mode 100644 index 00000000000..196d57a7a99 --- /dev/null +++ b/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy @@ -0,0 +1,66 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * The Gun Case Moved + * + * Author: SmartThings + */ +definition( + name: "The Gun Case Moved", + namespace: "smartthings", + author: "SmartThings", + description: "Send a text when your gun case moves", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer@2x.png" +) + +preferences { + section("When the gun case moves..."){ + input "accelerationSensor", "capability.accelerationSensor", title: "Where?" + } + section("Text me at..."){ + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone number?" + } + } +} + +def installed() { + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def updated() { + unsubscribe() + subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler) +} + +def accelerationActiveHandler(evt) { + // Don't send a continuous stream of text messages + def deltaSeconds = 5 + def timeAgo = new Date(now() - (1000 * deltaSeconds)) + def recentEvents = accelerationSensor.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds" + def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 + + if (alreadySentSms) { + log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + } else { + if (location.contactBookEnabled) { + sendNotificationToContacts("Gun case has moved!", recipients) + } + else { + log.debug "$accelerationSensor has moved, texting $phone1" + sendSms(phone1, "Gun case has moved!") + } + } +} diff --git a/smartapps/smartthings/turn-it-on-for-5-minutes.src/turn-it-on-for-5-minutes.groovy b/smartapps/smartthings/turn-it-on-for-5-minutes.src/turn-it-on-for-5-minutes.groovy new file mode 100644 index 00000000000..5da1ee6d2a3 --- /dev/null +++ b/smartapps/smartthings/turn-it-on-for-5-minutes.src/turn-it-on-for-5-minutes.groovy @@ -0,0 +1,56 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Turn It On For 5 Minutes + * Turn on a switch when a contact sensor opens and then turn it back off 5 minutes later. + * + * Author: SmartThings + */ +definition( + name: "Turn It On For 5 Minutes", + namespace: "smartthings", + author: "SmartThings", + description: "When a SmartSense Multi is opened, a switch will be turned on, and then turned off after 5 minutes.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png" +) + +preferences { + section("When it opens..."){ + input "contact1", "capability.contactSensor" + } + section("Turn on a switch for 5 minutes..."){ + input "switch1", "capability.switch" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def updated(settings) { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + switch1.on() + def fiveMinuteDelay = 60 * 5 + runIn(fiveMinuteDelay, turnOffSwitch) +} + +def turnOffSwitch() { + switch1.off() +} diff --git a/smartapps/smartthings/turn-it-on-when-im-here.src/turn-it-on-when-im-here.groovy b/smartapps/smartthings/turn-it-on-when-im-here.src/turn-it-on-when-im-here.groovy new file mode 100644 index 00000000000..d4d95a43752 --- /dev/null +++ b/smartapps/smartthings/turn-it-on-when-im-here.src/turn-it-on-when-im-here.groovy @@ -0,0 +1,62 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Turn It On When I'm Here + * + * Author: SmartThings + */ +definition( + name: "Turn It On When I'm Here", + namespace: "smartthings", + author: "SmartThings", + description: "Turn something on when you arrive and back off when you leave.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet@2x.png" +) + +preferences { + section("When I arrive and leave..."){ + input "presence1", "capability.presenceSensor", title: "Who?", multiple: true + } + section("Turn on/off a light..."){ + input "switch1", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(presence1, "presence", presenceHandler) +} + +def updated() +{ + unsubscribe() + subscribe(presence1, "presence", presenceHandler) +} + +def presenceHandler(evt) +{ + log.debug "presenceHandler $evt.name: $evt.value" + def current = presence1.currentValue("presence") + log.debug current + def presenceValue = presence1.find{it.currentPresence == "present"} + log.debug presenceValue + if(presenceValue){ + switch1.on() + log.debug "Someone's home!" + } + else{ + switch1.off() + log.debug "Everyone's away." + } +} diff --git a/smartapps/smartthings/turn-it-on-when-it-opens.src/turn-it-on-when-it-opens.groovy b/smartapps/smartthings/turn-it-on-when-it-opens.src/turn-it-on-when-it-opens.groovy new file mode 100644 index 00000000000..4a144c3bca9 --- /dev/null +++ b/smartapps/smartthings/turn-it-on-when-it-opens.src/turn-it-on-when-it-opens.groovy @@ -0,0 +1,53 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Turn It On When It Opens + * + * Author: SmartThings + */ +definition( + name: "Turn It On When It Opens", + namespace: "smartthings", + author: "SmartThings", + description: "Turn something on when an open/close sensor opens.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png" +) + +preferences { + section("When the door opens..."){ + input "contact1", "capability.contactSensor", title: "Where?" + } + section("Turn on a light..."){ + input "switches", "capability.switch", multiple: true + } +} + + +def installed() +{ + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contact1, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + log.debug "$evt.value: $evt, $settings" + log.trace "Turning on switches: $switches" + switches.on() +} + diff --git a/smartapps/smartthings/turn-on-only-if-i-arrive-after-sunset.src/turn-on-only-if-i-arrive-after-sunset.groovy b/smartapps/smartthings/turn-on-only-if-i-arrive-after-sunset.src/turn-on-only-if-i-arrive-after-sunset.groovy new file mode 100644 index 00000000000..0c04d4ca2a6 --- /dev/null +++ b/smartapps/smartthings/turn-on-only-if-i-arrive-after-sunset.src/turn-on-only-if-i-arrive-after-sunset.groovy @@ -0,0 +1,62 @@ +/** + * Turn On Only If I Arrive After Sunset + * + * Author: Danny De Leo + */ +definition( + name: "Turn On Only If I Arrive After Sunset", + namespace: "smartthings", + author: "SmartThings", + description: "Turn something on only if you arrive after sunset and back off anytime you leave.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet@2x.png" +) + +preferences { + section("When I arrive and leave..."){ + input "presence1", "capability.presenceSensor", title: "Who?", multiple: true + } + section("Turn on/off a light..."){ + input "switch1", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(presence1, "presence", presenceHandler) +} + +def updated() +{ + unsubscribe() + subscribe(presence1, "presence", presenceHandler) +} + +def presenceHandler(evt) +{ + def now = new Date() + def sunTime = getSunriseAndSunset(); + + log.debug "nowTime: $now" + log.debug "riseTime: $sunTime.sunrise" + log.debug "setTime: $sunTime.sunset" + log.debug "presenceHandler $evt.name: $evt.value" + + def current = presence1.currentValue("presence") + log.debug current + def presenceValue = presence1.find{it.currentPresence == "present"} + log.debug presenceValue + if(presenceValue && (now > sunTime.sunset)) { + switch1.on() + log.debug "Welcome home at night!" + } + else if(presenceValue && (now < sunTime.sunset)) { + log.debug "Welcome home at daytime!" + } + else { + switch1.off() + log.debug "Everyone's away." + } +} + diff --git a/smartapps/smartthings/ubi.src/ubi.groovy b/smartapps/smartthings/ubi.src/ubi.groovy new file mode 100644 index 00000000000..a00f5abfc00 --- /dev/null +++ b/smartapps/smartthings/ubi.src/ubi.groovy @@ -0,0 +1,532 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Ubi + * + * Author: SmartThings + */ + +definition( + name: "Ubi", + namespace: "smartthings", + author: "SmartThings", + description: "Add your Ubi device to your SmartThings Account", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ubi-app-icn.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ubi-app-icn@2x.png", + oauth: [displayName: "Ubi", displayLink: ""] +) + +preferences { + section("Allow a web application to control these things...") { + input name: "switches", type: "capability.switch", title: "Which Switches?", multiple: true, required: false + input name: "motions", type: "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false + input name: "locks", type: "capability.lock", title: "Which Locks?", multiple: true, required: false + input name: "contactSensors", type: "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false + input name: "presenceSensors", type: "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false + } +} + +mappings { + path("/list") { + action: [ + GET: "listAll" + ] + } + + path("/events/:id") { + action: [ + GET: "showEvents" + ] + } + + path("/switches") { + action: [ + GET: "listSwitches", + PUT: "updateSwitches", + POST: "updateSwitches" + ] + } + path("/switches/:id") { + action: [ + GET: "showSwitch", + PUT: "updateSwitch", + POST: "updateSwitch" + ] + } + path("/switches/subscriptions") { + log.debug "switches added" + action: [ + POST: "addSwitchSubscription" + ] + } + path("/switches/subscriptions/:id") { + action: [ + DELETE: "removeSwitchSubscription", + GET: "removeSwitchSubscription" + ] + } + + path("/motionSensors") { + action: [ + GET: "listMotions", + PUT: "updateMotions", + POST: "updateMotions" + + ] + } + path("/motionSensors/:id") { + action: [ + GET: "showMotion", + PUT: "updateMotion", + POST: "updateMotion" + ] + } + path("/motionSensors/subscriptions") { + log.debug "motionSensors added" + action: [ + POST: "addMotionSubscription" + ] + } + path("/motionSensors/subscriptions/:id") { + log.debug "motionSensors Deleted" + action: [ + DELETE: "removeMotionSubscription", + GET: "removeMotionSubscription" + ] + } + + path("/locks") { + action: [ + GET: "listLocks", + PUT: "updateLock", + POST: "updateLock" + ] + } + path("/locks/:id") { + action: [ + GET: "showLock", + PUT: "updateLock", + POST: "updateLock" + ] + } + path("/locks/subscriptions") { + action: [ + POST: "addLockSubscription" + ] + } + path("/locks/subscriptions/:id") { + action: [ + DELETE: "removeLockSubscription", + GET: "removeLockSubscription" + ] + } + + path("/contactSensors") { + action: [ + GET: "listContactSensors", + PUT: "updateContactSensor", + POST: "updateContactSensor" + ] + } + path("/contactSensors/:id") { + action: [ + GET: "showContactSensor", + PUT: "updateContactSensor", + POST: "updateContactSensor" + ] + } + path("/contactSensors/subscriptions") { + log.debug "contactSensors/subscriptions" + action: [ + POST: "addContactSubscription" + ] + } + path("/contactSensors/subscriptions/:id") { + action: [ + DELETE: "removeContactSensorSubscription", + GET: "removeContactSensorSubscription" + ] + } + + path("/presenceSensors") { + action: [ + GET: "listPresenceSensors", + PUT: "updatePresenceSensor", + POST: "updatePresenceSensor" + ] + } + path("/presenceSensors/:id") { + action: [ + GET: "showPresenceSensor", + PUT: "updatePresenceSensor", + POST: "updatePresenceSensor" + ] + } + path("/presenceSensors/subscriptions") { + log.debug "PresenceSensors/subscriptions" + action: [ + POST: "addPresenceSubscription" + ] + } + path("/presenceSensors/subscriptions/:id") { + action: [ + DELETE: "removePresenceSensorSubscription", + GET: "removePresenceSensorSubscription" + ] + } + + path("/state") { + action: [ + GET: "currentState" + ] + } + + path("/phrases") { + action: [ + GET: "listPhrases" + ] + } + path("/phrases/:phraseName") { + action: [ + GET: "executePhrase", + POST: "executePhrase", + ] + } + +} + +def installed() { +// subscribe(motions, "motion.active", motionOpenHandler) + +// subscribe(contactSensors, "contact.open", contactOpenHandler) +// log.trace "contactSensors Installed" + +} + +def updated() { + +// unsubscribe() +// subscribe(motions, "motion.active", motionOpenHandler) +// subscribe(contactSensors, "contact.open", contactOpenHandler) + +//log.trace "contactSensors Updated" + +} + +def listAll() { + listSwitches() + listMotions() + listLocks() + listContactSensors() + listPresenceSensors() + listPhrasesWithType() +} + +def listContactSensors() { + contactSensors.collect { device(it, "contactSensor") } +} + + +void updateContactSensors() { + updateAll(contactSensors) +} + +def showContactSensor() { + show(contactSensors, "contact") +} + +void updateContactSensor() { + update(contactSensors) +} + +def addContactSubscription() { + log.debug "addContactSensorSubscription, params: ${params}" + addSubscription(contactSensors, "contact") +} + +def removeContactSensorSubscription() { + removeSubscription(contactSensors) +} + + +def listPresenceSensors() { + presenceSensors.collect { device(it, "presenceSensor") } +} + + +void updatePresenceSensors() { + updateAll(presenceSensors) +} + +def showPresenceSensor() { + show(presenceSensors, "presence") +} + +void updatePresenceSensor() { + update(presenceSensors) +} + +def addPresenceSubscription() { + log.debug "addPresenceSensorSubscription, params: ${params}" + addSubscription(presenceSensors, "presence") +} + +def removePresenceSensorSubscription() { + removeSubscription(presenceSensors) +} + + +def listSwitches() { + switches.collect { device(it, "switch") } +} + +void updateSwitches() { + updateAll(switches) +} + +def showSwitch() { + show(switches, "switch") +} + +void updateSwitch() { + update(switches) +} + +def addSwitchSubscription() { + log.debug "addSwitchSubscription, params: ${params}" + addSubscription(switches, "switch") +} + +def removeSwitchSubscription() { + removeSubscription(switches) +} + +def listMotions() { + motions.collect { device(it, "motionSensor") } +} + +void updateMotions() { + updateAll(motions) +} + +def showMotion() { + show(motions, "motion") +} + +void updateMotion() { + update(motions) +} + +def addMotionSubscription() { + + addSubscription(motions, "motion") +} + +def removeMotionSubscription() { + removeSubscription(motions) +} + +def listLocks() { + locks.collect { device(it, "lock") } +} + +void updateLocks() { + updateAll(locks) +} + +def showLock() { + show(locks, "lock") +} + +void updateLock() { + update(locks) +} + +def addLockSubscription() { + addSubscription(locks, "lock") +} + +def removeLockSubscription() { + removeSubscription(locks) +} + +/* +def motionOpenHandler(evt) { +//log.trace "$evt.value: $evt, $settings" + + log.debug "$motions was active, sending push message to user" + //sendPush("Your ${contact1.label ?: contact1.name} was opened") + + + httpPostJson(uri: "http://automatesolutions.ca/test.php", path: '', body: [evt: [value: "motionSensor Active"]]) { + log.debug "Event data successfully posted" + } + +} +def contactOpenHandler(evt) { + //log.trace "$evt.value: $evt, $settings" + + log.debug "$contactSensors was opened, sending push message to user" + //sendPush("Your ${contact1.label ?: contact1.name} was opened") + + + httpPostJson(uri: "http://automatesolutions.ca/test.php", path: '', body: [evt: [value: "ContactSensor Opened"]]) { + log.debug "Event data successfully posted" + } + + +} +*/ + + +def deviceHandler(evt) { + log.debug "~~~~~TEST~~~~~~" + def deviceInfo = state[evt.deviceId] + if (deviceInfo) + { + httpPostJson(uri: deviceInfo.callbackUrl, path: '', body: [evt: [value: evt.value]]) { + log.debug "Event data successfully posted" + } + } + else + { + log.debug "No subscribed device found" + } +} + +def currentState() { + state +} + +def showStates() { + def device = (switches + motions + locks).find { it.id == params.id } + if (!device) + { + httpError(404, "Switch not found") + } + else + { + device.events(params) + } +} + +def listPhrasesWithType() { + location.helloHome.getPhrases().collect { + [ + "id" : it.id, + "label": it.label, + "type" : "phrase" + ] + } +} + +def listPhrases() { + location.helloHome.getPhrases().label +} + +def executePhrase() { + def phraseName = params.phraseName + if (phraseName) + { + location.helloHome.execute(phraseName) + log.debug "executed phrase: $phraseName" + } + else + { + httpError(404, "Phrase not found") + } +} + +private void updateAll(devices) { + def command = request.JSON?.command + if (command) + { + command = command.toLowerCase() + devices."$command"() + } +} + +private void update(devices) { + log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" + //def command = request.JSON?.command + def command = params.command + if (command) + { + command = command.toLowerCase() + def device = devices.find { it.id == params.id } + if (!device) + { + httpError(404, "Device not found") + } + else + { + device."$command"() + } + } +} + +private show(devices, type) { + def device = devices.find { it.id == params.id } + if (!device) + { + httpError(404, "Device not found") + } + else + { + def attributeName = type + + def s = device.currentState(attributeName) + [id: device.id, label: device.displayName, value: s?.value, unitTime: s?.date?.time, type: type] + } +} + +private addSubscription(devices, attribute) { + //def deviceId = request.JSON?.deviceId + //def callbackUrl = request.JSON?.callbackUrl + + log.debug "addSubscription, params: ${params}" + + def deviceId = params.deviceId + def callbackUrl = params.callbackUrl + + def myDevice = devices.find { it.id == deviceId } + if (myDevice) + { + log.debug "Adding switch subscription" + callbackUrl + state[deviceId] = [callbackUrl: callbackUrl] + log.debug "Added state: $state" + def subscription = subscribe(myDevice, attribute, deviceHandler) + if (subscription && subscription.eventSubscription) { + log.debug "Subscription is newly created" + } else { + log.debug "Subscription already exists, returning existing subscription" + subscription = app.subscriptions?.find { it.deviceId == deviceId && it.data == attribute && it.handler == 'deviceHandler' } + } + [ + id: subscription.id, + deviceId: subscription.deviceId, + data: subscription.data, + handler: subscription.handler, + callbackUrl: callbackUrl + ] + } +} + +private removeSubscription(devices) { + def deviceId = params.id + def device = devices.find { it.id == deviceId } + if (device) + { + log.debug "Removing $device.displayName subscription" + state.remove(device.id) + unsubscribe(device) + } +} + +private device(it, type) { + it ? [id: it.id, label: it.displayName, type: type] : null +} diff --git a/smartapps/smartthings/undead-early-warning.src/undead-early-warning.groovy b/smartapps/smartthings/undead-early-warning.src/undead-early-warning.groovy new file mode 100644 index 00000000000..f51fa5a6116 --- /dev/null +++ b/smartapps/smartthings/undead-early-warning.src/undead-early-warning.groovy @@ -0,0 +1,51 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * The simplest Undead Early Warning system that could possibly work. ;) + * + * Author: SmartThings + */ +definition( + name: "Undead Early Warning", + namespace: "smartthings", + author: "SmartThings", + description: "Undead Early Warning", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-UndeadEarlyWarning.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-UndeadEarlyWarning@2x.png" +) + +preferences { + section("When the door opens...") { + input "contacts", "capability.contactSensor", multiple: true, title: "Where could they come from?" + } + section("Turn on the lights!") { + input "switches", "capability.switch", multiple: true + } +} + +def installed() +{ + subscribe(contacts, "contact.open", contactOpenHandler) +} + +def updated() +{ + unsubscribe() + subscribe(contacts, "contact.open", contactOpenHandler) +} + +def contactOpenHandler(evt) { + log.debug "$evt.value: $evt, $settings" + log.trace "The Undead are coming! Turning on the lights: $switches" + switches.on() +} diff --git a/smartapps/smartthings/unlock-it-when-i-arrive.src/unlock-it-when-i-arrive.groovy b/smartapps/smartthings/unlock-it-when-i-arrive.src/unlock-it-when-i-arrive.groovy new file mode 100644 index 00000000000..fcac3314fd0 --- /dev/null +++ b/smartapps/smartthings/unlock-it-when-i-arrive.src/unlock-it-when-i-arrive.groovy @@ -0,0 +1,57 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Unlock It When I Arrive + * + * Author: SmartThings + * Date: 2013-02-11 + */ + +definition( + name: "Unlock It When I Arrive", + namespace: "smartthings", + author: "SmartThings", + description: "Unlocks the door when you arrive at your location.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + oauth: true +) + +preferences { + section("When I arrive..."){ + input "presence1", "capability.presenceSensor", title: "Who?", multiple: true + } + section("Unlock the lock..."){ + input "lock1", "capability.lock", multiple: true + } +} + +def installed() +{ + subscribe(presence1, "presence.present", presence) +} + +def updated() +{ + unsubscribe() + subscribe(presence1, "presence.present", presence) +} + +def presence(evt) +{ + def anyLocked = lock1.count{it.currentLock == "unlocked"} != lock1.size() + if (anyLocked) { + sendPush "Unlocked door due to arrival of $evt.displayName" + lock1.unlock() + } +} diff --git a/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy b/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy new file mode 100644 index 00000000000..15848200b87 --- /dev/null +++ b/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy @@ -0,0 +1,143 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Virtual Thermostat + * + * Author: SmartThings + */ +definition( + name: "Virtual Thermostat", + namespace: "smartthings", + author: "SmartThings", + description: "Control a space heater or window air conditioner in conjunction with any temperature sensor, like a SmartSense Multi.", + category: "Green Living", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png" +) + +preferences { + section("Choose a temperature sensor... "){ + input "sensor", "capability.temperatureMeasurement", title: "Sensor" + } + section("Select the heater or air conditioner outlet(s)... "){ + input "outlets", "capability.switch", title: "Outlets", multiple: true + } + section("Set the desired temperature..."){ + input "setpoint", "decimal", title: "Set Temp" + } + section("When there's been movement from (optional, leave blank to not require motion)..."){ + input "motion", "capability.motionSensor", title: "Motion", required: false + } + section("Within this number of minutes..."){ + input "minutes", "number", title: "Minutes", required: false + } + section("But never go below (or above if A/C) this value with or without motion..."){ + input "emergencySetpoint", "decimal", title: "Emer Temp", required: false + } + section("Select 'heat' for a heater and 'cool' for an air conditioner..."){ + input "mode", "enum", title: "Heating or cooling?", options: ["heat","cool"] + } +} + +def installed() +{ + subscribe(sensor, "temperature", temperatureHandler) + if (motion) { + subscribe(motion, "motion", motionHandler) + } +} + +def updated() +{ + unsubscribe() + subscribe(sensor, "temperature", temperatureHandler) + if (motion) { + subscribe(motion, "motion", motionHandler) + } +} + +def temperatureHandler(evt) +{ + def isActive = hasBeenRecentMotion() + if (isActive || emergencySetpoint) { + evaluate(evt.doubleValue, isActive ? setpoint : emergencySetpoint) + } + else { + outlets.off() + } +} + +def motionHandler(evt) +{ + if (evt.value == "active") { + def lastTemp = sensor.currentTemperature + if (lastTemp != null) { + evaluate(lastTemp, setpoint) + } + } else if (evt.value == "inactive") { + def isActive = hasBeenRecentMotion() + log.debug "INACTIVE($isActive)" + if (isActive || emergencySetpoint) { + def lastTemp = sensor.currentTemperature + if (lastTemp != null) { + evaluate(lastTemp, isActive ? setpoint : emergencySetpoint) + } + } + else { + outlets.off() + } + } +} + +private evaluate(currentTemp, desiredTemp) +{ + log.debug "EVALUATE($currentTemp, $desiredTemp)" + def threshold = 1.0 + if (mode == "cool") { + // air conditioner + if (currentTemp - desiredTemp >= threshold) { + outlets.on() + } + else if (desiredTemp - currentTemp >= threshold) { + outlets.off() + } + } + else { + // heater + if (desiredTemp - currentTemp >= threshold) { + outlets.on() + } + else if (currentTemp - desiredTemp >= threshold) { + outlets.off() + } + } +} + +private hasBeenRecentMotion() +{ + def isActive = false + if (motion && minutes) { + def deltaMinutes = minutes as Long + if (deltaMinutes) { + def motionEvents = motion.eventsSince(new Date(now() - (60000 * deltaMinutes))) + log.trace "Found ${motionEvents?.size() ?: 0} events in the last $deltaMinutes minutes" + if (motionEvents.find { it.value == "active" }) { + isActive = true + } + } + } + else { + isActive = true + } + isActive +} + diff --git a/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy b/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy new file mode 100644 index 00000000000..186685d03ac --- /dev/null +++ b/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy @@ -0,0 +1,481 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Wattvision Manager + * + * Author: steve + * Date: 2014-02-13 + */ + +// Automatically generated. Make future change here. +definition( + name: "Wattvision Manager", + namespace: "smartthings", + author: "SmartThings", + description: "Monitor your whole-house energy use by connecting to your Wattvision account", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision%402x.png", + oauth: [displayName: "Wattvision", displayLink: "https://www.wattvision.com/"] +) + +preferences { + page(name: "rootPage") +} + +def rootPage() { + def sensors = state.sensors + def hrefState = sensors ? "complete" : "" + def hrefDescription = "" + sensors.each { sensorId, sensorName -> + hrefDescription += "${sensorName}\n" + } + + dynamicPage(name: "rootPage", install: sensors ? true : false, uninstall: true) { + section { + href(url: loginURL(), title: "Connect Wattvision Sensors", style: "embedded", description: hrefDescription, state: hrefState) + } + section { + href(url: "https://www.wattvision.com", title: "Learn More About Wattvision", style: "external", description: null) + } + } +} + +mappings { + path("/access") { + actions: + [ + POST : "setApiAccess", + DELETE: "revokeApiAccess" + ] + } + path("/devices") { + actions: + [ + GET: "listDevices" + ] + } + path("/device/:sensorId") { + actions: + [ + GET : "getDevice", + PUT : "updateDevice", + POST : "createDevice", + DELETE: "deleteDevice" + ] + } + path("/${loginCallbackPath()}") { + actions: + [ + GET: "loginCallback" + ] + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + unschedule() + initialize() +} + +def initialize() { + getDataFromWattvision() + scheduleDataCollection() +} + +def getDataFromWattvision() { + + log.trace "Getting data from Wattvision" + + def children = getChildDevices() + if (!children) { + log.warn "No children. Not collecting data from Wattviwion" + // currently only support one child + return + } + + def endDate = new Date() + def startDate + + if (!state.lastUpdated) { +// log.debug "no state.lastUpdated" + startDate = new Date(hours: endDate.hours - 3) + } else { +// log.debug "parsing state.lastUpdated" + startDate = new Date().parse(smartThingsDateFormat(), state.lastUpdated) + } + + state.lastUpdated = endDate.format(smartThingsDateFormat()) + + children.each { child -> + getDataForChild(child, startDate, endDate) + } + +} + +def getDataForChild(child, startDate, endDate) { + if (!child) { + return + } + + def wattvisionURL = wattvisionURL(child.deviceNetworkId, startDate, endDate) + if (wattvisionURL) { + httpGet(uri: wattvisionURL) { response -> + def json = new org.json.JSONObject(response.data.toString()) + child.addWattvisionData(json) + return "success" + } + } +} + +def wattvisionURL(senorId, startDate, endDate) { + + log.trace "getting wattvisionURL" + + def wattvisionApiAccess = state.wattvisionApiAccess + if (!wattvisionApiAccess.id || !wattvisionApiAccess.key) { + return null + } + + if (!endDate) { + endDate = new Date() + } + if (!startDate) { + startDate = new Date(hours: endDate.hours - 3) + } + + def diff = endDate.getTime() - startDate.getTime() + if (diff > 259200000) { // 3 days in milliseconds + // Wattvision only allows pulling 3 hours of data at a time + startDate = new Date(hours: endDate.hours - 3) + } + + + def params = [ + "sensor_id" : senorId, + "api_id" : wattvisionApiAccess.id, + "api_key" : wattvisionApiAccess.key, + "type" : wattvisionDataType ?: "rate", + "start_time": startDate.format(wattvisionDateFormat()), + "end_time" : endDate.format(wattvisionDateFormat()) + ] + + def parameterString = params.collect { key, value -> "${key.encodeAsURL()}=${value.encodeAsURL()}" }.join("&") + def accessURL = wattvisionApiAccess.url ?: "https://www.wattvision.com/api/v0.2/elec" + def url = "${accessURL}?${parameterString}" + +// log.debug "wattvisionURL: ${url}" + return url +} + +def getData() { + state.lastUpdated = new Date().format(smartThingsDateFormat()) +} + +public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" } + +public wattvisionDateFormat() { "yyyy-MM-dd'T'HH:mm:ss" } + +def childMarshaller(child) { + return [ + name : child.name, + label : child.label, + sensor_id: child.deviceNetworkId, + location : child.location.name + ] +} + +// ======================================================== +// ENDPOINTS +// ======================================================== + +def listDevices() { + getChildDevices().collect { childMarshaller(it) } +} + +def getDevice() { + + log.trace "Getting device" + + def child = getChildDevice(params.sensorId) + + if (!child) { + httpError(404, "Device not found") + } + + return childMarshaller(child) +} + +def updateDevice() { + + log.trace "Updating Device with data from Wattvision" + + def body = request.JSON + + def child = getChildDevice(params.sensorId) + + if (!child) { + httpError(404, "Device not found") + } + + child.addWattvisionData(body) + + render([status: 204, data: " "]) +} + +def createDevice() { + + log.trace "Creating Wattvision device" + + if (getChildDevice(params.sensorId)) { + httpError(403, "Device already exists") + } + + def child = addChildDevice("smartthings", "Wattvision", params.sensorId, null, [name: "Wattvision", label: request.JSON.label]) + + child.setGraphUrl(getGraphUrl(params.sensorId)); + + getDataForChild(child, null, null) + + return childMarshaller(child) +} + +def deleteDevice() { + + log.trace "Deleting Wattvision device" + + deleteChildDevice(params.sensorId) + render([status: 204, data: " "]) +} + +def setApiAccess() { + + log.trace "Granting access to Wattvision API" + + def body = request.JSON + + state.wattvisionApiAccess = [ + url: body.url, + id : body.id, + key: body.key + ] + + scheduleDataCollection() + + render([status: 204, data: " "]) +} + +def scheduleDataCollection() { + schedule("* /1 * * * ?", "getDataFromWattvision") // every 1 minute +} + +def revokeApiAccess() { + + log.trace "Revoking access to Wattvision API" + + state.wattvisionApiAccess = [:] + render([status: 204, data: " "]) +} + +public getGraphUrl(sensorId) { + + log.trace "Collecting URL for Wattvision graph" + + def apiId = state.wattvisionApiAccess.id + def apiKey = state.wattvisionApiAccess.key + + // TODO: allow the changing of type? + "http://www.wattvision.com/partners/smartthings/charts?s=${sensorId}&api_id=${apiId}&api_key=${apiKey}&type=w" +} + +// ======================================================== +// SmartThings initiated setup +// ======================================================== + +/* Debug info for Steve / Andrew + +this page: /partners/smartthings/whatswv + - linked from within smartthings, will tell you how to get a wattvision sensor, etc. + - pass the debug flag (?debug=1) to show this text. + +login page: /partners/smartthings/login?callback_url=CALLBACKURL + - open this page, which will require login. + - once login is complete, we call you back at callback_url with: + ?id=&key= + question: will you know which user this is on your end? + +sensor json: /partners/smartthings/sensor_list?api_id=...&api_key=... + - returns a list of sensors and their associated house names, as a json object + - example return value with one sensor id 2, associated with house 'Test's House' + - content type is application/json + - {"2": "Test's House"} + +*/ + +def loginCallback() { + log.trace "loginCallback" + + state.wattvisionApiAccess = [ + id : params.id, + key: params.key + ] + + getSensorJSON(params.id, params.key) + + connectionSuccessful("Wattvision", "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision@2x.png") +} + +private getSensorJSON(id, key) { + log.trace "getSensorJSON" + + def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}" + + httpGet(uri: sensorUrl) { response -> + + def json = new org.json.JSONObject(response.data) + + state.sensors = json + + json.each { sensorId, sensorName -> + createChild(sensorId, sensorName) + } + + return "success" + } +} + +def createChild(sensorId, sensorName) { + log.trace "creating Wattvision Child" + + def child = getChildDevice(sensorId) + + if (child) { + log.warn "Device already exists" + } else { + child = addChildDevice("smartthings", "Wattvision", sensorId, null, [name: "Wattvision", label: sensorName]) + } + + child.setGraphUrl(getGraphUrl(sensorId)); + + getDataForChild(child, null, null) + + scheduleDataCollection() + + return childMarshaller(child) +} + +// ======================================================== +// URL HELPERS +// ======================================================== + +private loginURL() { "${wattvisionBaseURL()}${loginPath()}" } + +private wattvisionBaseURL() { "https://www.wattvision.com" } + +private loginPath() { "/partners/smartthings/login?callback_url=${loginCallbackURL().encodeAsURL()}" } + +private loginCallbackURL() { + if (!atomicState.accessToken) { createAccessToken() } + buildActionUrl(loginCallbackPath()) +} +private loginCallbackPath() { "login/callback" } + +// ======================================================== +// Access Token +// ======================================================== + +private getMyAccessToken() { return atomicState.accessToken ?: createAccessToken() } + +// ======================================================== +// CONNECTED HTML +// ======================================================== + +def connectionSuccessful(deviceName, iconSrc) { + def html = """ + + + + +Withings Connection + + + +
+ ${deviceName} icon + connected device icon + SmartThings logo +

Your ${deviceName} is now connected to SmartThings!

+

Click 'Done' to finish setup.

+
+ + +""" + + render contentType: 'text/html', data: html +} + diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy new file mode 100644 index 00000000000..34f20b188c7 --- /dev/null +++ b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy @@ -0,0 +1,652 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Wemo Service Manager + * + * Author: superuser + * Date: 2013-09-06 + */ +definition( + name: "Wemo (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png" +) + +preferences { + page(name:"firstPage", title:"Wemo Device Setup", content:"firstPage") +} + +private discoverAllWemoTypes() +{ + sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:Belkin:device:insight:1/urn:Belkin:device:controllee:1/urn:Belkin:device:sensor:1/urn:Belkin:device:lightswitch:1", physicalgraph.device.Protocol.LAN)) +} + +private getFriendlyName(String deviceNetworkId) { + sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1 +HOST: ${deviceNetworkId} + +""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) +} + +private verifyDevices() { + def switches = getWemoSwitches().findAll { it?.value?.verified != true } + def motions = getWemoMotions().findAll { it?.value?.verified != true } + def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified != true } + def devices = switches + motions + lightSwitches + devices.each { + getFriendlyName((it.value.ip + ":" + it.value.port)) + } +} + +def firstPage() +{ + if(canInstallLabs()) + { + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = 5 + + log.debug "REFRESH COUNT :: ${refreshCount}" + + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //ssdp request every 25 seconds + if((refreshCount % 5) == 0) { + discoverAllWemoTypes() + } + + //setup.xml request every 5 seconds except on discoveries + if(((refreshCount % 1) == 0) && ((refreshCount % 5) != 0)) { + verifyDevices() + } + + def switchesDiscovered = switchesDiscovered() + def motionsDiscovered = motionsDiscovered() + def lightSwitchesDiscovered = lightSwitchesDiscovered() + + return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: selectedSwitches != null || selectedMotions != null || selectedLightSwitches != null) { + section("Select a device...") { + input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered + input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered + input "selectedLightSwitches", "enum", required:false, title:"Select Wemo Light Switches \n(${lightSwitchesDiscovered.size() ?: 0} found)", multiple:true, options:lightSwitchesDiscovered + } + } + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"firstPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") { + paragraph "$upgradeNeeded" + } + } + } +} + +def devicesDiscovered() { + def switches = getWemoSwitches() + def motions = getWemoMotions() + def lightSwitches = getWemoLightSwitches() + def devices = switches + motions + lightSwitches + def list = [] + + list = devices?.collect{ [app.id, it.ssdpUSN].join('.') } +} + +def switchesDiscovered() { + def switches = getWemoSwitches().findAll { it?.value?.verified == true } + def map = [:] + switches.each { + def value = it.value.name ?: "WeMo Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}" + def key = it.value.mac + map["${key}"] = value + } + map +} + +def motionsDiscovered() { + def motions = getWemoMotions().findAll { it?.value?.verified == true } + def map = [:] + motions.each { + def value = it.value.name ?: "WeMo Motion ${it.value.ssdpUSN.split(':')[1][-3..-1]}" + def key = it.value.mac + map["${key}"] = value + } + map +} + +def lightSwitchesDiscovered() { + //def vmotions = switches.findAll { it?.verified == true } + //log.trace "MOTIONS HERE: ${vmotions}" + def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified == true } + def map = [:] + lightSwitches.each { + def value = it.value.name ?: "WeMo Light Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}" + def key = it.value.mac + map["${key}"] = value + } + map +} + +def getWemoSwitches() +{ + if (!state.switches) { state.switches = [:] } + state.switches +} + +def getWemoMotions() +{ + if (!state.motions) { state.motions = [:] } + state.motions +} + +def getWemoLightSwitches() +{ + if (!state.lightSwitches) { state.lightSwitches = [:] } + state.lightSwitches +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() + + runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds + runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds + runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes + + // SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes) + runIn(1740, "refresh", [overwrite: false]) +} + +def updated() { + log.debug "Updated with settings: ${settings}" + initialize() + + runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds + runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds +} + +def resubscribe() { + log.debug "Resubscribe called, delegating to refresh()" + refresh() +} + +def refresh() { + log.debug "refresh() called" + //reschedule the refreshes + runIn(1740, "refresh", [overwrite: false]) + refreshDevices() +} + +def refreshDevices() { + log.debug "refreshDevices() called" + def devices = getAllChildDevices() + devices.each { d -> + log.debug "Calling refresh() on device: ${d.id}" + d.refresh() + } +} + +def subscribeToDevices() { + log.debug "subscribeToDevices() called" + def devices = getAllChildDevices() + devices.each { d -> + d.subscribe() + } +} + +def addSwitches() { + def switches = getWemoSwitches() + + selectedSwitches.each { dni -> + def selectedSwitch = switches.find { it.value.mac == dni } ?: switches.find { "${it.value.ip}:${it.value.port}" == dni } + def d + if (selectedSwitch) { + d = getChildDevices()?.find { + it.dni == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac + } + } + + if (!d) { + log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}" + d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [ + "label": selectedSwitch?.value?.name ?: "Wemo Switch", + "data": [ + "mac": selectedSwitch.value.mac, + "ip": selectedSwitch.value.ip, + "port": selectedSwitch.value.port + ] + ]) + + log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + } +} + +def addMotions() { + def motions = getWemoMotions() + + selectedMotions.each { dni -> + def selectedMotion = motions.find { it.value.mac == dni } ?: motions.find { "${it.value.ip}:${it.value.port}" == dni } + def d + if (selectedMotion) { + d = getChildDevices()?.find { + it.dni == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac + } + } + + if (!d) { + log.debug "Creating WeMo Motion with dni: ${selectedMotion.value.mac}" + d = addChildDevice("smartthings", "Wemo Motion", selectedMotion.value.mac, selectedMotion?.value.hub, [ + "label": selectedMotion?.value?.name ?: "Wemo Motion", + "data": [ + "mac": selectedMotion.value.mac, + "ip": selectedMotion.value.ip, + "port": selectedMotion.value.port + ] + ]) + + log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + } +} + +def addLightSwitches() { + def lightSwitches = getWemoLightSwitches() + + selectedLightSwitches.each { dni -> + def selectedLightSwitch = lightSwitches.find { it.value.mac == dni } ?: lightSwitches.find { "${it.value.ip}:${it.value.port}" == dni } + def d + if (selectedLightSwitch) { + d = getChildDevices()?.find { + it.dni == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac + } + } + + if (!d) { + log.debug "Creating WeMo Light Switch with dni: ${selectedLightSwitch.value.mac}" + d = addChildDevice("smartthings", "Wemo Light Switch", selectedLightSwitch.value.mac, selectedLightSwitch?.value.hub, [ + "label": selectedLightSwitch?.value?.name ?: "Wemo Light Switch", + "data": [ + "mac": selectedLightSwitch.value.mac, + "ip": selectedLightSwitch.value.ip, + "port": selectedLightSwitch.value.port + ] + ]) + + log.debug "created ${d.displayName} with id $dni" + } else { + log.debug "found ${d.displayName} with id $dni already exists" + } + } +} + +def initialize() { + // remove location subscription afterwards + unsubscribe() + state.subscribe = false + + if (selectedSwitches) + { + addSwitches() + } + + if (selectedMotions) + { + addMotions() + } + + if (selectedLightSwitches) + { + addLightSwitches() + } +} + +def locationHandler(evt) { + def description = evt.description + def hub = evt?.hubId + def parsedEvent = parseDiscoveryMessage(description) + parsedEvent << ["hub":hub] + log.debug parsedEvent + + if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) { + + def switches = getWemoSwitches() + + if (!(switches."${parsedEvent.ssdpUSN.toString()}")) + { //if it doesn't already exist + switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } + else + { // just update the values + + log.debug "Device was already found in state..." + + def d = switches."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + log.debug "Device's port or ip changed..." + } + + if (deviceChangedValues) { + def children = getChildDevices() + log.debug "Found children ${children}" + children.each { + if (it.getDeviceDataByName("mac") == parsedEvent.mac) { + log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" + it.subscribe(parsedEvent.ip, parsedEvent.port) + } + } + } + + } + + } + else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) { + + def motions = getWemoMotions() + + if (!(motions."${parsedEvent.ssdpUSN.toString()}")) + { //if it doesn't already exist + motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } + else + { // just update the values + + log.debug "Device was already found in state..." + + def d = motions."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + log.debug "Device's port or ip changed..." + } + + if (deviceChangedValues) { + def children = getChildDevices() + log.debug "Found children ${children}" + children.each { + if (it.getDeviceDataByName("mac") == parsedEvent.mac) { + log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" + it.subscribe(parsedEvent.ip, parsedEvent.port) + } + } + } + } + + } + else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:lightswitch")) { + + def lightSwitches = getWemoLightSwitches() + + if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) + { //if it doesn't already exist + lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] + } + else + { // just update the values + + log.debug "Device was already found in state..." + + def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}" + boolean deviceChangedValues = false + + if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { + d.ip = parsedEvent.ip + d.port = parsedEvent.port + deviceChangedValues = true + log.debug "Device's port or ip changed..." + } + + if (deviceChangedValues) { + def children = getChildDevices() + log.debug "Found children ${children}" + children.each { + if (it.getDeviceDataByName("mac") == parsedEvent.mac) { + log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" + it.subscribe(parsedEvent.ip, parsedEvent.port) + } + } + } + + } + + } + else if (parsedEvent.headers && parsedEvent.body) { + String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase() + if (headerString != null && (headerString.contains('text/xml') || headerString.contains('application/xml'))) { + def body = parseXmlBody(parsedEvent.body) + if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:controllee:1")) + { + def switches = getWemoSwitches() + def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())} + if (wemoSwitch) + { + wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true] + } + else + { + log.error "/setup.xml returned a wemo device that didn't exist" + } + } + + if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:insight:1")) + { + def switches = getWemoSwitches() + def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())} + if (wemoSwitch) + { + wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true] + } + else + { + log.error "/setup.xml returned a wemo device that didn't exist" + } + } + + if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:sensor")) //?:1 + { + def motions = getWemoMotions() + def wemoMotion = motions.find {it?.key?.contains(body?.device?.UDN?.text())} + if (wemoMotion) + { + wemoMotion.value << [name:body?.device?.friendlyName?.text(), verified: true] + } + else + { + log.error "/setup.xml returned a wemo device that didn't exist" + } + } + + if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:lightswitch")) //?:1 + { + def lightSwitches = getWemoLightSwitches() + def wemoLightSwitch = lightSwitches.find {it?.key?.contains(body?.device?.UDN?.text())} + if (wemoLightSwitch) + { + wemoLightSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true] + } + else + { + log.error "/setup.xml returned a wemo device that didn't exist" + } + } + } + } +} + +private def parseXmlBody(def body) { + def decodedBytes = body.decodeBase64() + def bodyString + try { + bodyString = new String(decodedBytes) + } catch (Exception e) { + // Keep this log for debugging StringIndexOutOfBoundsException issue + log.error("Exception decoding bytes in sonos connect: ${decodedBytes}") + throw e + } + return new XmlSlurper().parseText(bodyString) +} + +private def parseDiscoveryMessage(String description) { + def device = [:] + def parts = description.split(',') + parts.each { part -> + part = part.trim() + if (part.startsWith('devicetype:')) { + def valueString = part.split(":")[1].trim() + device.devicetype = valueString + } + else if (part.startsWith('mac:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + device.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + device.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + device.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) { + def valueString = part.split(":")[1].trim() + if (valueString) { + device.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) { + device.ssdpUSN = valueString + } + } + else if (part.startsWith('ssdpTerm:')) { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) { + device.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) { + part -= "headers:" + def valueString = part.trim() + if (valueString) { + device.headers = valueString + } + } + else if (part.startsWith('body')) { + part -= "body:" + def valueString = part.trim() + if (valueString) { + device.body = valueString + } + } + } + + device +} + +def doDeviceSync(){ + log.debug "Doing Device Sync!" + runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes + + if(!state.subscribe) { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + discoverAllWemoTypes() +} + +def pollChildren() { + def devices = getAllChildDevices() + devices.each { d -> + //only poll switches? + d.poll() + } +} + +def delayPoll() { + log.debug "Executing 'delayPoll'" + + runIn(5, "pollChildren") +} + +/*def poll() { + log.debug "Executing 'poll'" + runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes + + def lastPoll = getLastPollTime() + def currentTime = now() + def lastPollDiff = currentTime - lastPoll + log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff" + setLastPollTime(currentTime) + + doDeviceSync() +} + + +def setLastPollTime(currentTime) { + state.lastpoll = currentTime +} + +def getLastPollTime() { + state.lastpoll ?: now() +} + +def now() { + new Date().getTime() +}*/ + +private Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} diff --git a/smartapps/smartthings/when-its-going-to-rain.src/when-its-going-to-rain.groovy b/smartapps/smartthings/when-its-going-to-rain.src/when-its-going-to-rain.groovy new file mode 100644 index 00000000000..f9751eea423 --- /dev/null +++ b/smartapps/smartthings/when-its-going-to-rain.src/when-its-going-to-rain.groovy @@ -0,0 +1,91 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * When It's Going To Rain + * + * Author: SmartThings + */ +definition( + name: "When It's Going to Rain", + namespace: "smartthings", + author: "SmartThings", + description: "Is your shed closed? Are your windows shut? Is the grill covered? Are your dogs indoors? Will the lawn and plants need to be watered tomorrow?", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png" +) + +preferences { + section("Zip code..."){ + input "zipcode", "text", title: "Zipcode?" + } + // TODO: would be nice to cron this so we could check every hour or so + section("Check at..."){ + input "time", "time", title: "When?" + } + section("Things to check..."){ + input "sensors", "capability.contactSensor", multiple: true + } + section("Text me if I anything is open..."){ + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone number?" + } + } +} + + +def installed() { + log.debug "Installed: $settings" + schedule(time, "scheduleCheck") +} + +def updated() { + log.debug "Updated: $settings" + unschedule() + schedule(time, "scheduleCheck") +} + +def scheduleCheck() { + def response = getWeatherFeature("forecast", zipcode) + if (isStormy(response)) { + def open = sensors.findAll { it?.latestValue("contact") == 'open' } + if (open) { + if (location.contactBookEnabled) { + sendNotificationToContacts("A storm is a coming and the following things are open: ${open.join(', ')}", recipients) + } + else { + sendSms(phone, "A storm is a coming and the following things are open: ${open.join(', ')}") + } + } + } +} + +private isStormy(json) { + def STORMY = ['rain', 'snow', 'showers', 'sprinkles', 'precipitation'] + + def forecast = json?.forecast?.txt_forecast?.forecastday?.first() + if (forecast) { + def text = forecast?.fcttext?.toLowerCase() + if (text) { + def result = false + for (int i = 0; i < STORMY.size() && !result; i++) { + result = text.contains(STORMY[i]) + } + return result + } else { + return false + } + } else { + log.warn "Did not get a forecast: $json" + return false + } +} diff --git a/smartapps/smartthings/withings.src/withings.groovy b/smartapps/smartthings/withings.src/withings.groovy new file mode 100644 index 00000000000..872e6539c77 --- /dev/null +++ b/smartapps/smartthings/withings.src/withings.groovy @@ -0,0 +1,578 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Withings Service Manager + * + * Author: SmartThings + * Date: 2013-09-26 + */ + +definition( + name: "Withings", + namespace: "smartthings", + author: "SmartThings", + description: "Connect your Withings scale to SmartThings.", + category: "Connections", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png", + oauth: true +) { + appSetting "clientId" + appSetting "clientSecret" + appSetting "serverUrl" +} + +preferences { + page(name: "auth", title: "Withings", content:"authPage") +} + +mappings { + path("/exchange") { + action: [ + GET: "exchangeToken" + ] + } + path("/load") { + action: [ + GET: "load" + ] + } +} + +def authPage() { + log.debug "authPage()" + dynamicPage(name: "auth", title: "Withings", install:false, uninstall:true) { + section { + paragraph "This version is no longer supported. Please uninstall it." + } + } +} + +def oauthInitUrl() { + def token = getToken() + log.debug "initiateOauth got token: $token" + + // store these for validate after the user takes the oauth journey + state.oauth_request_token = token.oauth_token + state.oauth_request_token_secret = token.oauth_token_secret + + return buildOauthUrlWithToken(token.oauth_token, token.oauth_token_secret) +} + +def getToken() { + def callback = getServerUrl() + "/api/smartapps/installations/${app.id}/exchange?access_token=${state.accessToken}" + def params = [ + oauth_callback:URLEncoder.encode(callback), + ] + def requestTokenBaseUrl = "https://oauth.withings.com/account/request_token" + def url = buildSignedUrl(requestTokenBaseUrl, params) + log.debug "getToken - url: $url" + + return getJsonFromUrl(url) +} + +def buildOauthUrlWithToken(String token, String tokenSecret) { + def callback = getServerUrl() + "/api/smartapps/installations/${app.id}/exchange?access_token=${state.accessToken}" + def params = [ + oauth_callback:URLEncoder.encode(callback), + oauth_token:token + ] + def authorizeBaseUrl = "https://oauth.withings.com/account/authorize" + + return buildSignedUrl(authorizeBaseUrl, params, tokenSecret) +} + +///////////////////////////////////////// +///////////////////////////////////////// +// vvv vvv OAuth 1.0 vvv vvv // +///////////////////////////////////////// +///////////////////////////////////////// +String buildSignedUrl(String baseUrl, Map urlParams, String tokenSecret="") { + def params = [ + oauth_consumer_key: smartThingsConsumerKey, + oauth_nonce: nonce(), + oauth_signature_method: "HMAC-SHA1", + oauth_timestamp: timestampInSeconds(), + oauth_version: 1.0 + ] + urlParams + def signatureBaseString = ["GET", baseUrl, toQueryString(params)].collect { URLEncoder.encode(it) }.join("&") + + params.oauth_signature = hmac(signatureBaseString, getSmartThingsConsumerSecret(), tokenSecret) + + // query string is different from what is used in generating the signature above b/c it includes "oauth_signature" + def url = [baseUrl, toQueryString(params)].join('?') + return url +} + +String nonce() { + return UUID.randomUUID().toString().replaceAll("-", "") +} + +Integer timestampInSeconds() { + return (int)(new Date().time/1000) +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +String hmac(String dataString, String consumerSecret, String tokenSecret="") throws java.security.SignatureException { + String result + + def key = [consumerSecret, tokenSecret].join('&') + + // get an hmac_sha1 key from the raw key bytes + def signingKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), "HmacSHA1") + + // get an hmac_sha1 Mac instance and initialize with the signing key + def mac = javax.crypto.Mac.getInstance("HmacSHA1") + mac.init(signingKey) + + // compute the hmac on input data bytes + byte[] rawHmac = mac.doFinal(dataString.getBytes()) + + result = org.apache.commons.codec.binary.Base64.encodeBase64String(rawHmac) + + return result +} +///////////////////////////////////////// +///////////////////////////////////////// +// ^^^ ^^^ OAuth 1.0 ^^^ ^^^ // +///////////////////////////////////////// +///////////////////////////////////////// + +///////////////////////////////////////// +///////////////////////////////////////// +// vvv vvv rest vvv vvv // +///////////////////////////////////////// +///////////////////////////////////////// + +protected rest(Map params) { + new physicalgraph.device.RestAction(params) +} + +///////////////////////////////////////// +///////////////////////////////////////// +// ^^^ ^^^ rest ^^^ ^^^ // +///////////////////////////////////////// +///////////////////////////////////////// + +def exchangeToken() { + // oauth_token=abcd + // &userid=123 + + def newToken = params.oauth_token + def userid = params.userid + def tokenSecret = state.oauth_request_token_secret + + def params = [ + oauth_token: newToken, + userid: userid + ] + + def requestTokenBaseUrl = "https://oauth.withings.com/account/access_token" + def url = buildSignedUrl(requestTokenBaseUrl, params, tokenSecret) + log.debug "signed url: $url with secret $tokenSecret" + + def token = getJsonFromUrl(url) + + state.userid = userid + state.oauth_token = token.oauth_token + state.oauth_token_secret = token.oauth_token_secret + + log.debug "swapped token" + + def location = getServerUrl() + "/api/smartapps/installations/${app.id}/load?access_token=${state.accessToken}" + redirect(location:location) +} + +def load() { + def json = get(getMeasurement(new Date() - 30)) + + log.debug "swapped, then received: $json" + parse(data:json) + + def html = """ + + + + +Withings Connection + + + +
+ withings icon + connected device icon + SmartThings logo +

Your Withings scale is now connected to SmartThings!

+

Click 'Done' to finish setup.

+
+ + +""" + + render contentType: 'text/html', data: html +} + +Map getJsonFromUrl(String url) { + return [:] // stop making requests to Withings API. This entire SmartApp will be replaced with a fix + + def jsonString + httpGet(uri: url) { resp -> + jsonString = resp.data.toString() + } + + return getJsonFromText(jsonString) +} + +Map getJsonFromText(String jsonString) { + def jsonMap = jsonString.split("&").inject([:]) { c, it -> + def parts = it.split('=') + def k = parts[0] + def v = parts[1] + c[k] = v + return c + } + + return jsonMap +} + +def getMeasurement(Date since=null) { + return null // stop making requests to Withings API. This entire SmartApp will be replaced with a fix + + // TODO: add startdate and enddate ... esp. when in response to notify + def params = [ + action:"getmeas", + oauth_consumer_key:getSmartThingsConsumerKey(), + oauth_nonce:nonce(), + oauth_signature_method:"HMAC-SHA1", + oauth_timestamp:timestampInSeconds(), + oauth_token:state.oauth_token, + oauth_version:1.0, + userid: state.userid + ] + + if(since) + { + params.startdate = dateToSeconds(since) + } + + def requestTokenBaseUrl = "http://wbsapi.withings.net/measure" + def signatureBaseString = ["GET", requestTokenBaseUrl, toQueryString(params)].collect { URLEncoder.encode(it) }.join("&") + + params.oauth_signature = hmac(signatureBaseString, getSmartThingsConsumerSecret(), state.oauth_token_secret) + + return rest( + method: 'GET', + endpoint: "http://wbsapi.withings.net", + path: "/measure", + query: params, + synchronous: true + ) + +} + +String get(measurementRestAction) { + return "" // stop making requests to Withings API. This entire SmartApp will be replaced with a fix + + def httpGetParams = [ + uri: measurementRestAction.endpoint, + path: measurementRestAction.path, + query: measurementRestAction.query + ] + + String json + httpGet(httpGetParams) {resp -> + json = resp.data.text.toString() + } + + return json +} + +def parse(Map response) { + def json = new org.codehaus.groovy.grails.web.json.JSONObject(response.data) + parseJson(json) +} + +def parseJson(json) { + log.debug "parseJson: $json" + + def lastDataPointMillis = (state.lastDataPointMillis ?: 0).toLong() + def i = 0 + + if(json.status == 0) + { + log.debug "parseJson measure group size: ${json.body.measuregrps.size()}" + + state.errorCount = 0 + + def childDni = getWithingsDevice(json.body.measuregrps).deviceNetworkId + + def latestMillis = lastDataPointMillis + json.body.measuregrps.sort { it.date }.each { group -> + + def measurementDateSeconds = group.date + def dataPointMillis = measurementDateSeconds * 1000L + + if(dataPointMillis > lastDataPointMillis) + { + group.measures.each { measure -> + i++ + saveMeasurement(childDni, measure, measurementDateSeconds) + } + } + + if(dataPointMillis > latestMillis) + { + latestMillis = dataPointMillis + } + + } + + if(latestMillis > lastDataPointMillis) + { + state.lastDataPointMillis = latestMillis + } + + def weightData = state.findAll { it.key.startsWith("measure.") } + + // remove old data + def old = "measure." + (new Date() - 30).format('yyyy-MM-dd') + state.findAll { it.key.startsWith("measure.") && it.key < old }.collect { it.key }.each { state.remove(it) } + } + else + { + def errorCount = (state.errorCount ?: 0).toInteger() + state.errorCount = errorCount + 1 + + // TODO: If we poll, consider waiting for a couple failures before showing an error + // But if we are only notified, then we need to raise the error right away + measurementError(json.status) + } + + log.debug "Done adding $i measurements" + return +} + +def measurementError(status) { + log.error "received api response status ${status}" + sendEvent(state.childDni, [name: "connection", value:"Connection error: ${status}", isStateChange:true, displayed:true]) +} + +def saveMeasurement(childDni, measure, measurementDateSeconds) { + def dateString = secondsToDate(measurementDateSeconds).format('yyyy-MM-dd') + + def measurement = withingsEvent(measure) + sendEvent(state.childDni, measurement + [date:dateString], [dateCreated:secondsToDate(measurementDateSeconds)]) + + log.debug "sm: ${measure.type} (${measure.type == 1})" + + if(measure.type == 6) + { + sendEvent(state.childDni, [name: "leanRatio", value:(100-measurement.value), date:dateString, isStateChange:true, display:true], [dateCreated:secondsToDate(measurementDateSeconds)]) + } + else if(measure.type == 1) + { + state["measure." + dateString] = measurement.value + } +} + +def eventValue(measure, roundDigits=1) { + def value = measure.value * 10.power(measure.unit) + + if(roundDigits != null) + { + def significantDigits = 10.power(roundDigits) + value = (value * significantDigits).toInteger() / significantDigits + } + + return value +} + +def withingsEvent(measure) { + def withingsTypes = [ + (1):"weight", + (4):"height", + (5):"leanMass", + (6):"fatRatio", + (8):"fatMass", + (11):"pulse" + ] + + def value = eventValue(measure, (measure.type == 4 ? null : 1)) + + if(measure.type == 1) { + value *= 2.20462 + } else if(measure.type == 4) { + value *= 39.3701 + } + + log.debug "m:${measure.type}, v:${value}" + + return [ + name: withingsTypes[measure.type], + value: value + ] +} + +Integer dateToSeconds(Date d) { + return d.time / 1000 +} + +Date secondsToDate(Number seconds) { + return new Date(seconds * 1000L) +} + +def getWithingsDevice(measuregrps=null) { + // unfortunately, Withings doesn't seem to give us enough information to know which device(s) they have, + // ... so we have to guess and create a single device + + if(state.childDni) + { + return getChildDevice(state.childDni) + } + else + { + def children = getChildDevices() + if(children.size() > 0) + { + return children[0] + } + else + { + // no child yet, create one + def dni = [app.id, UUID.randomUUID().toString()].join('.') + state.childDni = dni + + def childDeviceType = getBodyAnalyzerChildName() + + if(measuregrps) + { + def hasNoHeartRate = measuregrps.find { grp -> grp.measures.find { it.type == 11 } } == null + if(hasNoHeartRate) + { + childDeviceType = getScaleChildName() + } + } + + def child = addChildDevice(getChildNamespace(), childDeviceType, dni, null, [label:"Withings"]) + state.childId = child.id + return child + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + // TODO: subscribe to attributes, devices, locations, etc. +} + +def poll() { + if(shouldPoll()) + { + return getMeasurement() + } + + return null +} + +def shouldPoll() { + def lastPollString = state.lastPollMillisString + def lastPoll = lastPollString?.isNumber() ? lastPollString.toLong() : 0 + def ONE_HOUR = 60 * 60 * 1000 + + def time = new Date().time + + if(time > (lastPoll + ONE_HOUR)) + { + log.debug "Executing poll b/c (now > last + 1hr): ${time} > ${lastPoll + ONE_HOUR} (last: ${lastPollString})" + state.lastPollMillisString = time + + return true + } + + log.debug "skipping poll b/c !(now > last + 1hr): ${time} > ${lastPoll + ONE_HOUR} (last: ${lastPollString})" + return false +} + +def refresh() { + log.debug "Executing 'refresh'" + return getMeasurement() +} + +def getChildNamespace() { "smartthings" } +def getScaleChildName() { "Wireless Scale" } +def getBodyAnalyzerChildName() { "Smart Body Analyzer" } + +def getServerUrl() { appSettings.serverUrl } +def getSmartThingsConsumerKey() { appSettings.clientId } +def getSmartThingsConsumerSecret() { appSettings.clientSecret } diff --git a/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy b/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy new file mode 100644 index 00000000000..aebd0b56471 --- /dev/null +++ b/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy @@ -0,0 +1,751 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Yoics Service Manager + * + * Author: SmartThings + * Date: 2013-11-19 + */ + +definition( + name: "Yoics (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Connect and Control your Yoics Enabled Devices", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + oauth: true +) { + appSetting "serverUrl" +} + +preferences { + page(name: "auth", title: "Sign in", content: "authPage", uninstall:true) + page(name: "page2", title: "Yoics Devices", install:true, content: "listAvailableCameras") +} + + +mappings { + path("/foauth") { + action: [ + GET: "foauth" + ] + } + path("/authorize") { + action: [ + POST: "authorize" + ] + } + +} + +def authPage() +{ + log.debug "authPage()" + + if(!state.accessToken) + { + log.debug "about to create access token" + createAccessToken() + } + + + def description = "Required" + + if(getAuthHashValueIsValid()) + { + // TODO: Check if it's valid + if(true) + { + description = "Already saved" + } + else + { + description = "Required" + } + } + + def redirectUrl = buildUrl("", "foauth") + + return dynamicPage(name: "auth", title: "Yoics", nextPage:"page2") { + section("Yoics Login"){ + href url:redirectUrl, style:"embedded", required:false, title:"Yoics", description:description + } + } + +} + +def buildUrl(String key, String endpoint="increment", Boolean absolute=true) +{ + if(key) { + key = "/${key}" + } + + def url = "/api/smartapps/installations/${app.id}/${endpoint}${key}?access_token=${state.accessToken}" + + if (q) { + url += "q=${q}" + } + + if(absolute) + { + url = serverUrl + url + } + + return url +} + +//Deprecated +def getServerName() { + return getServerUrl() +} + +def getServerUrl() { + return appSettings.serverUrl +} + +def listAvailableCameras() { + + //def loginResult = forceLogin() + + //if(loginResult.success) + //{ + state.cameraNames = [:] + + def cameras = getDeviceList().inject([:]) { c, it -> + def dni = [app.id, it.uuid].join('.') + def cameraName = it.title ?: "Yoics" + + state.cameraNames[dni] = cameraName + c[dni] = cameraName + + return c + } + + return dynamicPage(name: "page2", title: "Yoics Devices", install:true) { + section("Select which Yoics Devices to connect"){ + input(name: "cameras", title:"", type: "enum", required:false, multiple:true, metadata:[values:cameras]) + } + section("Turn on which Lights when taking pictures") + { + input "switches", "capability.switch", multiple: true, required:false + } + } + //} + /*else + { + log.error "login result false" + return [errorMessage:"There was an error logging in to Dropcam"] + }*/ + +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def uninstalled() { + removeChildDevices(getChildDevices()) +} + +def initialize() { + + if(!state.suppressDelete) + { + state.suppressDelete = [:] + } + + log.debug "settings: $settings" + + def devices = cameras.collect { dni -> + + def name = state.cameraNames[dni] ?: "Yoics Device" + + def d = getChildDevice(dni) + + if(!d) + { + d = addChildDevice("smartthings", "Yoics Camera", dni, null, [name:"YoicsCamera", label:name]) + + /* WE'LL GET PROXY ON TAKE REQUEST + def setupProxyResult = setupProxy(dni) + if(setupProxyResult.success) + { + log.debug "Setting up the proxy worked...taking image capture now?" + + } + */ + + //Let's not take photos on add + //d.take() + + log.debug "created ${d.displayName} with id $dni" + } + else + { + log.debug "found ${d.displayName} with id $dni already exists" + } + + return d + } + + log.debug "created ${devices.size()} dropcams" + + /* //Original Code seems to delete the dropcam that is being added */ + + // Delete any that are no longer in settings + def delete = getChildDevices().findAll { !cameras?.contains(it.deviceNetworkId) } + removeChildDevices(delete) +} + +private removeChildDevices(delete) +{ + log.debug "deleting ${delete.size()} dropcams" + delete.each { + state.suppressDelete[it.deviceNetworkId] = true + deleteChildDevice(it.deviceNetworkId) + state.suppressDelete.remove(it.deviceNetworkId) + } +} +private List getDeviceList() +{ + + //https://apilb.yoics.net/web/api/getdevices.ashx?token=&filter=all&whose=me&state=%20all&type=xml + + def deviceListParams = [ + uri: "https://apilb.yoics.net", + path: "/web/api/getdevices.ashx", + headers: ['User-Agent': validUserAgent()], + requestContentType: "application/json", + query: [token: getLoginTokenValue(), filter: "all", whose: "me", state: "all", type:"json" ] + ] + + log.debug "cam list via: $deviceListParams" + + def multipleHtml + def singleUrl + def something + def more + + def devices = [] + + httpGet(deviceListParams) { resp -> + + log.debug "getting device list..." + + something = resp.status + more = "headers: " + resp.headers.collect { "${it.name}:${it.value}" } + + if(resp.status == 200) + { + def jsonString = resp.data.str + def body = new groovy.json.JsonSlurper().parseText(jsonString) + + //log.debug "get devices list response: ${jsonString}" + //log.debug "get device list response: ${body}" + + body.NewDataSet.Table.each { d -> + //log.debug "Addding ${d.devicealias} with address: ${d.deviceaddress}" + devices << [title: d.devicealias, uuid: d.deviceaddress] //uuid should be another name + } + + } + else + { + // ERROR + log.error "camera list: unknown response" + } + + } + + log.debug "list: after getting cameras: " + [devices:devices, url:singleUrl, html:multipleHtml?.size(), something:something, more:more] + + // ERROR? + return devices +} + +def removeChildFromSettings(child) +{ + def device = child.device + + def dni = device.deviceNetworkId + log.debug "removing child device $device with dni ${dni}" + + if(!state?.suppressDelete?.get(dni)) + { + def newSettings = settings.cameras?.findAll { it != dni } ?: [] + app.updateSetting("cameras", newSettings) + } +} + +private forceLogin() { + updateAuthHash(null) + login() +} + + +private login() { + + if(getAuthHashValueIsValid()) + { + return [success:true] + } + return doLogin() +} + +/*private setupProxy(dni) { + //https://apilb.yoics.net/web/api/connect.ashx?token=&deviceaddress=00:00:48:02:2A:A2:08:0E&type=xml + + def address = dni?.split(/\./)?.last() + + def loginParams = [ + uri: "https://apilb.yoics.net", + path: "/web/api/connect.ashx", + headers: ['User-Agent': validUserAgent()], + requestContentType: "application/json", + query: [token: getLoginTokenValue(), deviceaddress:address, type:"json" ] + ] + + def result = [success:false] + + httpGet(loginParams) { resp -> + if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json") + { + log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" } + def jsonString = resp.data.str + def body = new groovy.json.JsonSlurper().parseText(jsonString) + + def proxy = body?.NewDataSet?.Table[0]?.proxy + def requested = body?.NewDataSet?.Table[0]?.requested + def expirationsec = body?.NewDataSet?.Table[0]?.expirationsec + def url = body?.NewDataSet?.Table[0]?.url + + def proxyMap = [proxy:proxy, requested: requested, expirationsec:expirationsec, url: url] + + if (proxy) { + //log.debug "setting ${dni} proxy to ${proxyMap}" + //updateDeviceProxy(address, proxyMap) + result.success = true + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + + + } + + return result +}*/ + + + +private doLogin(user = "", pwd = "") { //change this name + + def loginParams = [ + uri: "https://apilb.yoics.net", + path: "/web/api/login.ashx", + headers: ['User-Agent': validUserAgent()], + requestContentType: "application/json", + query: [key: "SmartThingsApplication", usr: username, pwd: password, apilevel: 12, type:"json" ] + ] + + if (user) { + loginParams.query = [key: "SmartThingsApplication", usr: user, pwd: pwd, apilevel: 12, type:"json" ] + } + + def result = [success:false] + + httpGet(loginParams) { resp -> + if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json") + { + log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" } + def jsonString = resp.data.str + def body = new groovy.json.JsonSlurper().parseText(jsonString) + + log.debug "login response: ${jsonString}" + log.debug "login response: ${body}" + + def authhash = body?.NewDataSet?.Table[0]?.authhash //.token + + //this may return as well?? + def token = body?.NewDataSet?.Table[0]?.token ?: null + + if (authhash) { + log.debug "login setting authhash to ${authhash}" + updateAuthHash(authhash) + if (token) { + log.debug "login setting login token to ${token}" + updateLoginToken(token) + result.success = true + } else { + result.success = doLoginToken() + } + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + + + } + + return result +} + +private doLoginToken() { + + def loginParams = [ + uri: "https://apilb.yoics.net", + path: "/web/api/login.ashx", + headers: ['User-Agent': validUserAgent()], + requestContentType: "application/json", + query: [key: "SmartThingsApplication", usr: getUserName(), auth: getAuthHashValue(), apilevel: 12, type:"json" ] + ] + + def result = [success:false] + + httpGet(loginParams) { resp -> + if (resp.status == 200) + { + log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" } + + def jsonString = resp.data.str + def body = new groovy.json.JsonSlurper().parseText(jsonString) + + def token = body?.NewDataSet?.Table[0]?.token + + if (token) { + log.debug "login setting login to $token" + updateLoginToken(token) + result.success = true + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + + + } + + return result +} + +def takePicture(String dni, Integer imgWidth=null) +{ + + //turn on any of the selected lights that are off + def offLights = switches.findAll{(it.currentValue("switch") == "off")} + log.debug offLights + offLights.collect{it.on()} + + log.debug "parent.takePicture(${dni}, ${imgWidth})" + + def uuid = dni?.split(/\./)?.last() + + log.debug "taking picture for $uuid (${dni})" + + def imageBytes + def loginRequired = false + + try + { + imageBytes = doTakePicture(uuid, imgWidth) + } + catch(Exception e) + { + log.error "Exception $e trying to take a picture, attempting to login again" + loginRequired = true + } + + if(loginRequired) + { + def loginResult = doLoginToken() + if(loginResult.success) + { + // try once more + imageBytes = doTakePicture(uuid, imgWidth) + } + else + { + log.error "tried to login to dropcam after failing to take a picture and failed" + } + } + + //turn previously off lights to their original state + offLights.collect{it.off()} + return imageBytes +} + +private doTakePicture(String uuid, Integer imgWidth) +{ + imgWidth = imgWidth ?: 1280 + def loginRequired = false + + def proxyParams = getDeviceProxy(uuid) + if(!proxyParams.success) + { + throw new Exception("Login Required") + } + + def takeParams = [ + uri: "${proxyParams.uri}", + path: "${proxyParams.path}", + headers: ['User-Agent': validUserAgent()] + ] + + def imageBytes + + httpGet(takeParams) { resp -> + + if(resp.status == 403) + { + loginRequired = true + } + else if (resp.status == 200 && resp.headers.'Content-Type'.contains("image/jpeg")) + { + imageBytes = resp.data + } + else + { + log.error "unknown takePicture() response: ${resp.status} - ${resp.headers.'Content-Type'}" + } + } + + if(loginRequired) + { + throw new Exception("Login Required") + } + + return imageBytes +} + +///////////////////////// +private Boolean getLoginTokenValueIsValid() +{ + return getLoginTokenValue() +} + +private updateLoginToken(String token) { + state.loginToken = token +} + +private getLoginTokenValue() { + state.loginToken +} + +private Boolean getAuthHashValueIsValid() +{ + return getAuthHashValue() +} + +private updateAuthHash(String hash) { + state.authHash = hash +} + +private getAuthHashValue() { + state.authHash +} + +private updateUserName(String username) { + state.username = username +} + +private getUserName() { + state.username +} + +/*private getDeviceProxy(dni){ + //check if it exists or is not longer valid and create a new proxy here + log.debug "returning proxy ${state.proxy[dni].proxy}" + def proxy = [uri:state.proxy[dni].proxy, path:state.proxy[dni].url] + log.debug "returning proxy ${proxy}" + proxy +}*/ + +private updateDeviceProxy(dni, map){ + if (!state.proxy) { state.proxy = [:] } + state.proxy[dni] = map +} + +private getDeviceProxy(dni) { + def address = dni?.split(/\./)?.last() + + def loginParams = [ + uri: "https://apilb.yoics.net", + path: "/web/api/connect.ashx", + headers: ['User-Agent': validUserAgent()], + requestContentType: "application/json", + query: [token: getLoginTokenValue(), deviceaddress:address, type:"json" ] + ] + + def result = [success:false] + + httpGet(loginParams) { resp -> + if (resp.status == 200) //&& resp.headers.'Content-Type'.contains("application/json") + { + log.debug "login 200 json headers: " + resp.headers.collect { "${it.name}:${it.value}" } + def jsonString = resp.data.str + def body = new groovy.json.JsonSlurper().parseText(jsonString) + + if (body?.NewDataSet?.Table[0]?.error) + { + log.error "Attempt to get Yoics Proxy failed" + // ERROR: any more information we can give? + result.reason = body?.NewDataSet?.Table[0]?.message + } + else + { + result.uri = body?.NewDataSet?.Table[0]?.proxy + result.path = body?.NewDataSet?.Table[0]?.url + result.requested = body?.NewDataSet?.Table[0]?.requested + result.expirationsec = body?.NewDataSet?.Table[0]?.expirationsec + result.success = true + } + + } + else + { + // ERROR: any more information we can give? + result.reason = "Bad login" + } + + + } + + return result + +} + +private validUserAgent() { + "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5" +} + +def foauth() { + def html = """ + + $inputQuery results + + + + + + + + + + + + + +

+ Yoics Login +

+ +
+User: +
+ +
+
+Password: +
+ + + +
+ + + +""" + + render status: 200, contentType: 'text/html', data: html +} + +def authorize() { + + def loginResult = doLogin(params.user, params.password) + + def result + if (loginResult.success) { + result = "Successful" + + //save username + updateUserName(params.user) + } else { + result = "Failed" + } + + def html = """ + + $inputQuery results + + + + + + + + + + + + + + + + +

+ Yoics Login ${result}! +

+ + +""" + + render status: 200, contentType: 'text/html', data: html +} diff --git a/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy b/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy new file mode 100644 index 00000000000..54f38ab5a94 --- /dev/null +++ b/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy @@ -0,0 +1,155 @@ +/** + * Sprayer Controller 2 + * + * Copyright 2014 Cooper Lee + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Sprayer Controller 2", + namespace: "", + author: "Cooper Lee", + description: "Control Sprayers for a period of time a number of times per hour", + category: "My Apps", + iconUrl: "http://www.mountpleasantwaterworks.com/images/ground_sprinkler.png", + iconX2Url: "http://www.mountpleasantwaterworks.com/images/ground_sprinkler.png" +) + + +preferences { + section("Select First Valve(s):") { + input name: "valves1", type: "capability.switch", multiple: true + input name: "startHour1", title: "Start Hour", type: "number" + input name: "stopHour1", title: "Stop Hour", type: "number" + input "minutes", "enum", title: "Run how many times an Hour?", expanded: true, + options: ["1","2","3","4","5","6","12","20","30","60"] /*/ + options: ["0", "0,30", "0,20,40", "0,15,30,45", "0, 10, 15, 20, 25,30,35,40,45,50,55", "6", "7"] */ + input "duration", "number", title: "For how many seconds?" + } + +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + def startHour = startHour1 + def stopHour = stopHour1 + def startTime = minutes + if (minutes == "1") { + startTime = "0 0 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "2") { + startTime = "0 0,30 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "3") { + startTime = "0 0,20,40 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "4") { + startTime = "0 0,15,30,45 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "5") { + startTime = "0 0,12,24,36,48 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "6") { + startTime = "0 0,10,20,30,40,50 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "12") { + startTime = "0 0,5,10,15,20,25,30,35,40,45,50,55 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "20") { + startTime = "0 0,3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "30") { + startTime = "0 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58 " + startHour + "-" + stopHour + " * * ?" + } else { + startTime = "0 0 " + startHour + "-" + stopHour + " * * ?" + } + log.debug "${startTime}" + /* + def stopTime = "0 $minutes $stopHour * * ?" */ + schedule(startTime, openValve) +/* schedule("0 0,5,10,15,20,25,30,35,40,45,50,55 " + startHour + "-" + stopHour + " * * ?", openValve) */ +/* schedule(stopTime, closeValve) */ + subscribe(valves1, "switch.on", valveOnHandler, [filterEvents: false]) + +} + +def updated(settings) { + unschedule() + unsubscribe() + log.debug "Installed with settings: ${settings}" + def startHour = startHour1 + def stopHour = stopHour1 + def startTime = minutes + if (minutes == "1") { + startTime = "0 0 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "2") { + startTime = "0 0,30 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "3") { + startTime = "0 0,20,40 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "4") { + startTime = "0 0,15,30,45 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "5") { + startTime = "0 0,12,24,36,48 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "6") { + startTime = "0 0,10,20,30,40,50 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "12") { + startTime = "0 0,5,10,15,20,25,30,35,40,45,50,55 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "20") { + startTime = "0 0,3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57 " + startHour + "-" + stopHour + " * * ?" + } else if (minutes == "30") { + startTime = "0 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58 " + startHour + "-" + stopHour + " * * ?" + } else { + startTime = "0 0 " + startHour + "-" + stopHour + " * * ?" + } + log.debug "${startTime}" + /* + def stopTime = "0 $minutes $stopHour * * ?" */ + schedule(startTime, openValve) +/* schedule(stopTime, closeValve) */ + subscribe(valves1, "switch.on", valveOnHandler, [filterEvents: false]) +/* schedule("0 0,5,10,15,20,25,30,35,40,45,50,55 " + startHour + "-" + stopHour + " * * ?", openValve) */ + +} + +def openValve() { + log.debug "Turning on Sprinklers ${valves1}" + valves1.on() + +} + +def closeValve() { + log.debug "Turning off Sprinklers ${valves1}" + valves1.off() +} + +def valveOnHandler(evt) { + log.debug "Valve ${valves1} turned: ${evt.value}" + def delay = duration + log.debug "Turning off in ${duration/60} minutes (${delay}seconds)" + runIn(delay, closeValve) +} + +def setStartTime() { + if (minutes == "1") { + def startTime = "0 0 $startHour * * ?" + } else if (minutes == "2") { + def startTime = "0 0,30 $startHour * * ?" + } else if (minutes == "3") { + def startTime = "0 0,20,40 $startHour * * ?" + } else if (minutes == "4") { + def startTime = "0 0,15,30,45 $startHour * * ?" + } else if (minutes == "5") { + def startTime = "0 0,12,24,36,48 $startHour * * ?" + } else if (minutes == "6") { + def startTime = "0 0,10,20,30,40,50 $startHour * * ?" + } else if (minutes == "12") { + def startTime = "0 0,5,10,15,20,25,30,35,40,45,50,55 $startHour * * ?" + } else if (minutes == "20") { + def startTime = "0 0,3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57 $startHour * * ?" + } else if (minutes == "30") { + def startTime = "0 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58 $startHour * * ?" + } else { + def startTime = "0 0 $startHour * * ?" + } +} diff --git a/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy b/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy new file mode 100644 index 00000000000..89306d1aede --- /dev/null +++ b/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy @@ -0,0 +1,1852 @@ +/** + * Smart Alarm is a multi-zone virtual alarm panel, featuring customizable + * security zones. Setting of an alarm can activate sirens, turn on light + * switches, push notification and text message. Alarm is armed and disarmed + * simply by setting SmartThings location 'mode'. + * + * Please visit for more + * information. + * + * Version 2.4.3 (7/7/2015) + * + * The latest version of this file can be found on GitHub at: + * + * + * -------------------------------------------------------------------------- + * + * Copyright (c) 2014 Statusbits.com + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +import groovy.json.JsonSlurper + +definition( + name: "Smart Alarm", + namespace: "statusbits", + author: "geko@statusbits.com", + description: '''A multi-zone virtual alarm panel, featuring customizable\ + security zones. Setting of an alarm can activate sirens, turn on light\ + switches, push notification and text message. Alarm is armed and disarmed\ + simply by setting SmartThings location 'mode'.''', + category: "Safety & Security", + iconUrl: "http://statusbits.github.io/icons/SmartAlarm-128.png", + iconX2Url: "http://statusbits.github.io/icons/SmartAlarm-256.png", + oauth: [displayName:"Smart Alarm", displayLink:"http://statusbits.github.io/smartalarm/"] +) + +preferences { + page name:"pageSetup" + page name:"pageAbout" + page name:"pageUninstall" + page name:"pageStatus" + page name:"pageHistory" + page name:"pageSelectZones" + page name:"pageConfigureZones" + page name:"pageArmingOptions" + page name:"pageAlarmOptions" + page name:"pageNotifications" + page name:"pageRemoteOptions" + page name:"pageRestApiOptions" +} + +mappings { + path("/armaway") { + action: [ GET: "apiArmAway" ] + } + + path("/armaway/:pincode") { + action: [ GET: "apiArmAway" ] + } + + path("/armstay") { + action: [ GET: "apiArmStay" ] + } + + path("/armstay/:pincode") { + action: [ GET: "apiArmStay" ] + } + + path("/disarm") { + action: [ GET: "apiDisarm" ] + } + + path("/disarm/:pincode") { + action: [ GET: "apiDisarm" ] + } + + path("/panic") { + action: [ GET: "apiPanic" ] + } + + path("/status") { + action: [ GET: "apiStatus" ] + } +} + +// Show setup page +def pageSetup() { + LOG("pageSetup()") + + if (state.version != getVersion()) { + return setupInit() ? pageAbout() : pageUninstall() + } + + if (getNumZones() == 0) { + return pageSelectZones() + } + + def alarmStatus = "Alarm is ${getAlarmStatus()}" + + def pageProperties = [ + name: "pageSetup", + //title: "Status", + nextPage: null, + install: true, + uninstall: state.installed + ] + + return dynamicPage(pageProperties) { + section("Status") { + if (state.zones.size() > 0) { + href "pageStatus", title:alarmStatus, description:"Tap for more information" + } else { + paragraph alarmStatus + } + if (state.history.size() > 0) { + href "pageHistory", title:"Event History", description:"Tap to view" + } + } + section("Setup Menu") { + href "pageSelectZones", title:"Add/Remove Zones", description:"Tap to open" + href "pageConfigureZones", title:"Configure Zones", description:"Tap to open" + href "pageArmingOptions", title:"Arming/Disarming Options", description:"Tap to open" + href "pageAlarmOptions", title:"Alarm Options", description:"Tap to open" + href "pageNotifications", title:"Notification Options", description:"Tap to open" + href "pageRemoteOptions", title:"Remote Control Options", description:"Tap to open" + href "pageRestApiOptions", title:"REST API Options", description:"Tap to open" + href "pageAbout", title:"About Smart Alarm", description:"Tap to open" + } + section([title:"Options", mobileOnly:true]) { + label title:"Assign a name", required:false + } + } +} + +// Show "About" page +def pageAbout() { + LOG("pageAbout()") + + def textAbout = + "Version ${getVersion()}\n${textCopyright()}\n\n" + + "You can contribute to the development of this app by making " + + "donation to geko@statusbits.com via PayPal." + + def hrefInfo = [ + url: "http://statusbits.github.io/smartalarm/", + style: "embedded", + title: "Tap here for more information...", + description:"http://statusbits.github.io/smartalarm/", + required: false + ] + + def pageProperties = [ + name: "pageAbout", + //title: "About", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("About") { + paragraph textAbout + href hrefInfo + } + section("License") { + paragraph textLicense() + } + } +} + +// Show "Uninstall" page +def pageUninstall() { + LOG("pageUninstall()") + + def text = + "Smart Alarm version ${getVersion()} is not backward compatible " + + "with the currently installed version. Please uninstall the " + + "current version by tapping the Uninstall button below, then " + + "re-install Smart Alarm from the Dashboard. We are sorry for the " + + "inconvenience." + + def pageProperties = [ + name: "pageUninstall", + title: "Warning!", + nextPage: null, + uninstall: true, + install: false + ] + + return dynamicPage(pageProperties) { + section("Uninstall Required") { + paragraph text + } + } +} + +// Show "Status" page +def pageStatus() { + LOG("pageStatus()") + + def pageProperties = [ + name: "pageStatus", + //title: "Status", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Status") { + paragraph "Alarm is ${getAlarmStatus()}" + } + + if (settings.z_contact) { + section("Contact Sensors") { + settings.z_contact.each() { + def text = getZoneStatus(it, "contact") + if (text) { + paragraph text + } + } + } + } + + if (settings.z_motion) { + section("Motion Sensors") { + settings.z_motion.each() { + def text = getZoneStatus(it, "motion") + if (text) { + paragraph text + } + } + } + } + + if (settings.z_movement) { + section("Movement Sensors") { + settings.z_movement.each() { + def text = getZoneStatus(it, "acceleration") + if (text) { + paragraph text + } + } + } + } + + if (settings.z_smoke) { + section("Smoke & CO Sensors") { + settings.z_smoke.each() { + def text = getZoneStatus(it, "smoke") + if (text) { + paragraph text + } + } + } + } + + if (settings.z_water) { + section("Moisture Sensors") { + settings.z_water.each() { + def text = getZoneStatus(it, "water") + if (text) { + paragraph text + } + } + } + } + } +} + +// Show "History" page +def pageHistory() { + LOG("pageHistory()") + + def pageProperties = [ + name: "pageHistory", + //title: "Event History", + nextPage: "pageSetup", + uninstall: false + ] + + def history = atomicState.history + + return dynamicPage(pageProperties) { + section("Event History") { + if (history.size() == 0) { + paragraph "No history available." + } else { + paragraph "Not implemented" + } + } + } +} + +// Show "Add/Remove Zones" page +def pageSelectZones() { + LOG("pageSelectZones()") + + def helpPage = + "A security zone is an area of your property protected by a sensor " + + "(contact, motion, movement, moisture or smoke)." + + def inputContact = [ + name: "z_contact", + type: "capability.contactSensor", + title: "Which contact sensors?", + multiple: true, + required: false + ] + + def inputMotion = [ + name: "z_motion", + type: "capability.motionSensor", + title: "Which motion sensors?", + multiple: true, + required: false + ] + + def inputMovement = [ + name: "z_movement", + type: "capability.accelerationSensor", + title: "Which movement sensors?", + multiple: true, + required: false + ] + + def inputSmoke = [ + name: "z_smoke", + type: "capability.smokeDetector", + title: "Which smoke & CO sensors?", + multiple: true, + required: false + ] + + def inputMoisture = [ + name: "z_water", + type: "capability.waterSensor", + title: "Which moisture sensors?", + multiple: true, + required: false + ] + + def pageProperties = [ + name: "pageSelectZones", + //title: "Add/Remove Zones", + nextPage: "pageConfigureZones", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Add/Remove Zones") { + paragraph helpPage + input inputContact + input inputMotion + input inputMovement + input inputSmoke + input inputMoisture + } + } +} + +// Show "Configure Zones" page +def pageConfigureZones() { + LOG("pageConfigureZones()") + + def helpZones = + "Security zones can be configured as either Exterior, Interior, " + + "Alert or Bypass. Exterior zones are armed in both Away and Stay " + + "modes, while Interior zones are armed only in Away mode, allowing " + + "you to move freely inside the premises while the alarm is armed " + + "in Stay mode. Alert zones are always armed and are typically used " + + "for smoke and flood alarms. Bypass zones are never armed. This " + + "allows you to temporarily exclude a zone from your security " + + "system.\n\n" + + "You can disable Entry and Exit Delays for individual zones." + + def zoneTypes = ["exterior", "interior", "alert", "bypass"] + + def pageProperties = [ + name: "pageConfigureZones", + //title: "Configure Zones", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Configure Zones") { + paragraph helpZones + } + + if (settings.z_contact) { + def devices = settings.z_contact.sort {it.displayName} + devices.each() { + def devId = it.id + section("${it.displayName} (contact)") { + input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"exterior" + input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:true + } + } + } + + if (settings.z_motion) { + def devices = settings.z_motion.sort {it.displayName} + devices.each() { + def devId = it.id + section("${it.displayName} (motion)") { + input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"interior" + input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false + } + } + } + + if (settings.z_movement) { + def devices = settings.z_movement.sort {it.displayName} + devices.each() { + def devId = it.id + section("${it.displayName} (movement)") { + input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"interior" + input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false + } + } + } + + if (settings.z_smoke) { + def devices = settings.z_smoke.sort {it.displayName} + devices.each() { + def devId = it.id + section("${it.displayName} (smoke)") { + input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"alert" + input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false + } + } + } + + if (settings.z_water) { + def devices = settings.z_water.sort {it.displayName} + devices.each() { + def devId = it.id + section("${it.displayName} (moisture)") { + input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"alert" + input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false + } + } + } + } +} + +// Show "Arming/Disarming Options" page +def pageArmingOptions() { + LOG("pageArmingOptions()") + + def helpArming = + "Smart Alarm can be armed and disarmed by setting the home Mode. " + + "There are two arming modes - Stay and Away. Interior zones are " + + "not armed in Stay mode, allowing you to move freely inside your " + + "home." + + def helpDelay = + "Exit and entry delay allows you to exit the premises after arming " + + "your alarm system and enter the premises while the alarm system " + + "is armed without setting off an alarm. You can optionally disable " + + "entry and exit delay when the alarm is armed in Stay mode." + + def inputAwayModes = [ + name: "awayModes", + type: "mode", + title: "Arm 'Away' in these Modes", + multiple: true, + required: false + ] + + def inputStayModes = [ + name: "stayModes", + type: "mode", + title: "Arm 'Stay' in these Modes", + multiple: true, + required: false + ] + + def inputDisarmModes = [ + name: "disarmModes", + type: "mode", + title: "Disarm in these Modes", + multiple: true, + required: false + ] + + def inputDelay = [ + name: "delay", + type: "enum", + metadata: [values:["30","45","60","90"]], + title: "Delay (in seconds)", + defaultValue: "30", + required: true + ] + + def inputDelayStay = [ + name: "stayDelayOff", + type: "bool", + title: "Disable delays in Stay mode", + defaultValue: false, + required: true + ] + + def pageProperties = [ + name: "pageArmingOptions", + //title: "Arming/Disarming Options", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Arming/Disarming Options") { + paragraph helpArming + } + + section("Modes") { + input inputAwayModes + input inputStayModes + input inputDisarmModes + } + + section("Exit and Entry Delay") { + paragraph helpDelay + input inputDelay + input inputDelayStay + } + } +} + +// Show "Alarm Options" page +def pageAlarmOptions() { + LOG("pageAlarmOptions()") + + def helpAlarm = + "You can configure Smart Alarm to take several actions when an " + + "alarm is set off, such as turning on sirens and light switches, " + + "taking camera snapshots and executing a 'Hello, Home' action." + + def inputAlarms = [ + name: "alarms", + type: "capability.alarm", + title: "Which sirens?", + multiple: true, + required: false + ] + + def inputSirenMode = [ + name: "sirenMode", + type: "enum", + metadata: [values:["Off","Siren","Strobe","Both"]], + title: "Choose siren mode", + defaultValue: "Both" + ] + + def inputSwitches = [ + name: "switches", + type: "capability.switch", + title: "Which switches?", + multiple: true, + required: false + ] + + def inputCameras = [ + name: "cameras", + type: "capability.imageCapture", + title: "Which cameras?", + multiple: true, + required: false + ] + + def hhActions = getHelloHomeActions() + def inputHelloHome = [ + name: "helloHomeAction", + type: "enum", + title: "Which 'Hello, Home' action?", + metadata: [values: hhActions], + required: false + ] + + def pageProperties = [ + name: "pageAlarmOptions", + //title: "Alarm Options", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Alarm Options") { + paragraph helpAlarm + } + section("Sirens") { + input inputAlarms + input inputSirenMode + } + section("Switches") { + input inputSwitches + } + section("Cameras") { + input inputCameras + } + section("'Hello, Home' Actions") { + input inputHelloHome + } + } +} + +// Show "Notification Options" page +def pageNotifications() { + LOG("pageNotifications()") + + def helpAbout = + "You can configure Smart Alarm to notify you when it is armed, " + + "disarmed or when an alarm is set off. Notifications can be send " + + "using either Push messages, SMS (text) messages and Pushbullet " + + "messaging service. Smart Alarm can also notify you with sounds or " + + "voice alerts using compatible audio devices, such as Sonos." + + def inputPushAlarm = [ + name: "pushMessage", + type: "bool", + title: "Notify on Alarm", + defaultValue: true + ] + + def inputPushStatus = [ + name: "pushStatusMessage", + type: "bool", + title: "Notify on Status Change", + defaultValue: true + ] + + def inputPhone1 = [ + name: "phone1", + type: "phone", + title: "Send to this number", + required: false + ] + + def inputPhone1Alarm = [ + name: "smsAlarmPhone1", + type: "bool", + title: "Notify on Alarm", + defaultValue: false + ] + + def inputPhone1Status = [ + name: "smsStatusPhone1", + type: "bool", + title: "Notify on Status Change", + defaultValue: false + ] + + def inputPhone2 = [ + name: "phone2", + type: "phone", + title: "Send to this number", + required: false + ] + + def inputPhone2Alarm = [ + name: "smsAlarmPhone2", + type: "bool", + title: "Notify on Alarm", + defaultValue: false + ] + + def inputPhone2Status = [ + name: "smsStatusPhone2", + type: "bool", + title: "Notify on Status Change", + defaultValue: false + ] + + def inputPhone3 = [ + name: "phone3", + type: "phone", + title: "Send to this number", + required: false + ] + + def inputPhone3Alarm = [ + name: "smsAlarmPhone3", + type: "bool", + title: "Notify on Alarm", + defaultValue: false + ] + + def inputPhone3Status = [ + name: "smsStatusPhone3", + type: "bool", + title: "Notify on Status Change", + defaultValue: false + ] + + def inputPhone4 = [ + name: "phone4", + type: "phone", + title: "Send to this number", + required: false + ] + + def inputPhone4Alarm = [ + name: "smsAlarmPhone4", + type: "bool", + title: "Notify on Alarm", + defaultValue: false + ] + + def inputPhone4Status = [ + name: "smsStatusPhone4", + type: "bool", + title: "Notify on Status Change", + defaultValue: false + ] + + def inputPushbulletDevice = [ + name: "pushbullet", + type: "device.pushbullet", + title: "Which Pushbullet devices?", + multiple: true, + required: false + ] + + def inputPushbulletAlarm = [ + name: "pushbulletAlarm", + type: "bool", + title: "Notify on Alarm", + defaultValue: true + ] + + def inputPushbulletStatus = [ + name: "pushbulletStatus", + type: "bool", + title: "Notify on Status Change", + defaultValue: true + ] + + def inputAudioPlayers = [ + name: "audioPlayer", + type: "capability.musicPlayer", + title: "Which audio players?", + multiple: true, + required: false + ] + + def inputSpeechOnAlarm = [ + name: "speechOnAlarm", + type: "bool", + title: "Notify on Alarm", + defaultValue: true + ] + + def inputSpeechOnStatus = [ + name: "speechOnStatus", + type: "bool", + title: "Notify on Status Change", + defaultValue: true + ] + + def inputSpeechTextAlarm = [ + name: "speechText", + type: "text", + title: "Alarm Phrase", + required: false + ] + + def inputSpeechTextArmedAway = [ + name: "speechTextArmedAway", + type: "text", + title: "Armed Away Phrase", + required: false + ] + + def inputSpeechTextArmedStay = [ + name: "speechTextArmedStay", + type: "text", + title: "Armed Stay Phrase", + required: false + ] + + def inputSpeechTextDisarmed = [ + name: "speechTextDisarmed", + type: "text", + title: "Disarmed Phrase", + required: false + ] + + def pageProperties = [ + name: "pageNotifications", + //title: "Notification Options", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Notification Options") { + paragraph helpAbout + } + section("Push Notifications") { + input inputPushAlarm + input inputPushStatus + } + section("Text Message (SMS) #1") { + input inputPhone1 + input inputPhone1Alarm + input inputPhone1Status + } + section("Text Message (SMS) #2") { + input inputPhone2 + input inputPhone2Alarm + input inputPhone2Status + } + section("Text Message (SMS) #3") { + input inputPhone3 + input inputPhone3Alarm + input inputPhone3Status + } + section("Text Message (SMS) #4") { + input inputPhone4 + input inputPhone4Alarm + input inputPhone4Status + } + section("Pushbullet Notifications") { + input inputPushbulletDevice + input inputPushbulletAlarm + input inputPushbulletStatus + } + section("Audio Notifications") { + input inputAudioPlayers + input inputSpeechOnAlarm + input inputSpeechOnStatus + input inputSpeechTextAlarm + input inputSpeechTextArmedAway + input inputSpeechTextArmedStay + input inputSpeechTextDisarmed + } + } +} + +// Show "Remote Control Options" page +def pageRemoteOptions() { + LOG("pageRemoteOptions()") + + def helpRemote = + "You can arm and disarm Smart Alarm using any compatible remote " + + "control, for example Aeon Labs Minimote." + + def inputRemotes = [ + name: "remotes", + type: "capability.button", + title: "Which remote controls?", + multiple: true, + required: false + ] + + def inputArmAwayButton = [ + name: "buttonArmAway", + type: "number", + title: "Which button?", + required: false + ] + + def inputArmAwayHold = [ + name: "holdArmAway", + type: "bool", + title: "Hold to activate", + defaultValue: false, + required: true + ] + + def inputArmStayButton = [ + name: "buttonArmStay", + type: "number", + title: "Which button?", + required: false + ] + + def inputArmStayHold = [ + name: "holdArmStay", + type: "bool", + title: "Hold to activate", + defaultValue: false, + required: true + ] + + def inputDisarmButton = [ + name: "buttonDisarm", + type: "number", + title: "Which button?", + required: false + ] + + def inputDisarmHold = [ + name: "holdDisarm", + type: "bool", + title: "Hold to activate", + defaultValue: false, + required: true + ] + + def inputPanicButton = [ + name: "buttonPanic", + type: "number", + title: "Which button?", + required: false + ] + + def inputPanicHold = [ + name: "holdPanic", + type: "bool", + title: "Hold to activate", + defaultValue: false, + required: true + ] + + def pageProperties = [ + name: "pageRemoteOptions", + //title: "Remote Control Options", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("Remote Control Options") { + paragraph helpRemote + input inputRemotes + } + + section("Arm Away Button") { + input inputArmAwayButton + input inputArmAwayHold + } + + section("Arm Stay Button") { + input inputArmStayButton + input inputArmStayHold + } + + section("Disarm Button") { + input inputDisarmButton + input inputDisarmHold + } + + section("Panic Button") { + input inputPanicButton + input inputPanicHold + } + } +} + +// Show "REST API Options" page +def pageRestApiOptions() { + LOG("pageRestApiOptions()") + + def textHelp = + "Smart Alarm can be controlled remotely by any Web client using " + + "REST API. Please refer to Smart Alarm documentation for more " + + "information." + + def textPincode = + "You can specify optional PIN code to protect arming and disarming " + + "Smart Alarm via REST API from unauthorized access. If set, the " + + "PIN code is always required for disarming Smart Alarm, however " + + "you can optionally turn it off for arming Smart Alarm." + + def inputRestApi = [ + name: "restApiEnabled", + type: "bool", + title: "Enable REST API", + defaultValue: false + ] + + def inputPincode = [ + name: "pincode", + type: "number", + title: "PIN Code", + required: false + ] + + def inputArmWithPin = [ + name: "armWithPin", + type: "bool", + title: "Require PIN code to arm", + defaultValue: true + ] + + def pageProperties = [ + name: "pageRestApiOptions", + //title: "REST API Options", + nextPage: "pageSetup", + uninstall: false + ] + + return dynamicPage(pageProperties) { + section("REST API Options") { + paragraph textHelp + input inputRestApi + } + + section("PIN Code") { + paragraph textPincode + input inputPincode + input inputArmWithPin + } + + if (isRestApiEnabled()) { + section("REST API Info") { + paragraph "App ID:\n${app.id}" + paragraph "Access Token:\n${state.accessToken}" + } + } + } +} + +def installed() { + LOG("installed()") + + initialize() + state.installed = true +} + +def updated() { + LOG("updated()") + + unschedule() + unsubscribe() + initialize() +} + +private def setupInit() { + LOG("setupInit()") + + if (state.installed == null) { + state.installed = false + state.armed = false + state.zones = [] + state.alarms = [] + state.history = [] + } else { + def version = state.version as String + if (version == null || version.startsWith('1')) { + return false + } + } + + state.version = getVersion() + return true +} + +private def initialize() { + log.info "Smart Alarm. Version ${getVersion()}. ${textCopyright()}" + LOG("settings: ${settings}") + + clearAlarm() + state.delay = settings.delay?.toInteger() ?: 30 + state.offSwitches = [] + state.history = [] + + if (settings.awayModes?.contains(location.mode)) { + state.armed = true + state.stay = false + } else if (settings.stayModes?.contains(location.mode)) { + state.armed = true + state.stay = true + } else { + state.armed = false + state.stay = false + } + + initZones() + initButtons() + initRestApi() + subscribe(location, onLocation) + + STATE() +} + +private def clearAlarm() { + LOG("clearAlarm()") + + state.alarms = [] + settings.alarms*.off() + + // Turn off only those switches that we've turned on + def switchesOff = state.offSwitches + if (switchesOff) { + LOG("switchesOff: ${switchesOff}") + settings.switches.each() { + if (switchesOff.contains(it.id)) { + it.off() + } + } + state.offSwitches = [] + } +} + +private def initZones() { + LOG("initZones()") + + state.zones = [] + + state.zones << [ + deviceId: null, + sensorType: "panic", + zoneType: "alert", + delay: false + ] + + if (settings.z_contact) { + settings.z_contact.each() { + state.zones << [ + deviceId: it.id, + sensorType: "contact", + zoneType: settings["type_${it.id}"] ?: "exterior", + delay: settings["delay_${it.id}"] + ] + } + subscribe(settings.z_contact, "contact.open", onContact) + } + + if (settings.z_motion) { + settings.z_motion.each() { + state.zones << [ + deviceId: it.id, + sensorType: "motion", + zoneType: settings["type_${it.id}"] ?: "interior", + delay: settings["delay_${it.id}"] + ] + } + subscribe(settings.z_motion, "motion.active", onMotion) + } + + if (settings.z_movement) { + settings.z_movement.each() { + state.zones << [ + deviceId: it.id, + sensorType: "acceleration", + zoneType: settings["type_${it.id}"] ?: "interior", + delay: settings["delay_${it.id}"] + ] + } + subscribe(settings.z_movement, "acceleration.active", onMovement) + } + + if (settings.z_smoke) { + settings.z_smoke.each() { + state.zones << [ + deviceId: it.id, + sensorType: "smoke", + zoneType: settings["type_${it.id}"] ?: "alert", + delay: settings["delay_${it.id}"] + ] + } + subscribe(settings.z_smoke, "smoke.detected", onSmoke) + subscribe(settings.z_smoke, "smoke.tested", onSmoke) + subscribe(settings.z_smoke, "carbonMonoxide.detected", onSmoke) + subscribe(settings.z_smoke, "carbonMonoxide.tested", onSmoke) + } + + if (settings.z_water) { + settings.z_water.each() { + state.zones << [ + deviceId: it.id, + sensorType: "water", + zoneType: settings["type_${it.id}"] ?: "alert", + delay: settings["delay_${it.id}"] + ] + } + subscribe(settings.z_water, "water.wet", onWater) + } + + state.zones.each() { + def zoneType = it.zoneType + + if (zoneType == "alert") { + it.armed = true + } else if (zoneType == "exterior") { + it.armed = state.armed + } else if (zoneType == "interior") { + it.armed = state.armed && !state.stay + } else { + it.armed = false + } + } +} + +private def initButtons() { + LOG("initButtons()") + + state.buttonActions = [] + if (settings.remotes) { + if (settings.buttonArmAway) { + def button = settings.buttonArmAway.toInteger() + def event = settings.holdArmAway ? "held" : "pushed" + state.buttonActions << [button:button, event:event, action:"armAway"] + } + + if (settings.buttonArmStay) { + def button = settings.buttonArmStay.toInteger() + def event = settings.holdArmStay ? "held" : "pushed" + state.buttonActions << [button:button, event:event, action:"armStay"] + } + + if (settings.buttonDisarm) { + def button = settings.buttonDisarm.toInteger() + def event = settings.holdDisarm ? "held" : "pushed" + state.buttonActions << [button:button, event:event, action:"disarm"] + } + + if (settings.buttonPanic) { + def button = settings.buttonPanic.toInteger() + def event = settings.holdPanic ? "held" : "pushed" + state.buttonActions << [button:button, event:event, action:"panic"] + } + + if (state.buttonActions) { + subscribe(settings.remotes, "button", onButtonEvent) + } + } +} + +private def initRestApi() { + if (settings.restApiEnabled) { + if (!state.accessToken) { + def token = createAccessToken() + LOG("Created new access token: ${token})") + } + state.url = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/" + log.info "REST API enabled" + } else { + state.url = "" + log.info "REST API disabled" + } +} + +private def isRestApiEnabled() { + return settings.restApiEnabled && state.accessToken +} + +def onContact(evt) { onZoneEvent(evt, "contact") } +def onMotion(evt) { onZoneEvent(evt, "motion") } +def onMovement(evt) { onZoneEvent(evt, "acceleration") } +def onSmoke(evt) { onZoneEvent(evt, "smoke") } +def onWater(evt) { onZoneEvent(evt, "water") } + +private def onZoneEvent(evt, sensorType) { + LOG("onZoneEvent(${evt.displayName}, ${sensorType})") + + def zone = getZoneForDevice(evt.deviceId, sensorType) + if (!zone) { + log.warn "Cannot find zone for device ${evt.deviceId}" + return + } + + if (zone.armed) { + state.alarms << evt.displayName + if (zone.zoneType == "alert" || !zone.delay || (state.stay && settings.stayDelayOff)) { + activateAlarm() + } else { + myRunIn(state.delay, activateAlarm) + } + } +} + +def onLocation(evt) { + LOG("onLocation(${evt.value})") + + String mode = evt.value + if (settings.awayModes?.contains(mode)) { + armAway() + } else if (settings.stayModes?.contains(mode)) { + armStay() + } else if (settings.disarmModes?.contains(mode)) { + disarm() + } +} + +def onButtonEvent(evt) { + LOG("onButtonEvent(${evt.displayName})") + + if (!state.buttonActions || !evt.data) { + return + } + + def slurper = new JsonSlurper() + def data = slurper.parseText(evt.data) + def button = data.buttonNumber?.toInteger() + if (button) { + LOG("Button '${button}' was ${evt.value}.") + def item = state.buttonActions.find { + it.button == button && it.event == evt.value + } + + if (item) { + LOG("Executing '${item.action}' button action") + "${item.action}"() + } + } +} + +def armAway() { + LOG("armAway()") + + if (!atomicState.armed || atomicState.stay) { + armPanel(false) + } +} + +def armStay() { + LOG("armStay()") + + if (!atomicState.armed || !atomicState.stay) { + armPanel(true) + } +} + +def disarm() { + LOG("disarm()") + + if (atomicState.armed) { + state.armed = false + state.zones.each() { + if (it.zoneType != "alert") { + it.armed = false + } + } + + reset() + } +} + +def panic() { + LOG("panic()") + + state.alarms << "Panic" + activateAlarm() +} + +def reset() { + LOG("reset()") + + unschedule() + clearAlarm() + + // Send notification + def msg = "${location.name} is " + if (state.armed) { + msg += "ARMED " + msg += state.stay ? "STAY" : "AWAY" + } else { + msg += "DISARMED." + } + + notify(msg) + notifyVoice() +} + +def exitDelayExpired() { + LOG("exitDelayExpired()") + + def armed = atomicState.armed + def stay = atomicState.stay + if (!armed) { + log.warn "exitDelayExpired: unexpected state!" + STATE() + return + } + + state.zones.each() { + def zoneType = it.zoneType + if (zoneType == "exterior" || (zoneType == "interior" && !stay)) { + it.armed = true + } + } + + def msg = "${location.name}: all " + if (stay) { + msg += "exterior " + } + msg += "zones are armed." + + notify(msg) +} + +private def armPanel(stay) { + LOG("armPanel(${stay})") + + unschedule() + clearAlarm() + + state.armed = true + state.stay = stay + + def armDelay = false + state.zones.each() { + def zoneType = it.zoneType + if (zoneType == "exterior") { + if (it.delay) { + it.armed = false + armDelay = true + } else { + it.armed = true + } + } else if (zoneType == "interior") { + if (stay) { + it.armed = false + } else if (it.delay) { + it.armed = false + armDelay = true + } else { + it.armed = true + } + } + } + + def delay = armDelay && !(stay && settings.stayDelayOff) ? atomicState.delay : 0 + if (delay) { + myRunIn(delay, exitDelayExpired) + } + + def mode = stay ? "STAY" : "AWAY" + def msg = "${location.name} " + if (delay) { + msg += "will arm ${mode} in ${state.delay} seconds." + } else { + msg += "is ARMED ${mode}." + } + + notify(msg) + notifyVoice() +} + +// .../armaway REST API endpoint +def apiArmAway() { + LOG("apiArmAway()") + + if (!isRestApiEnabled()) { + log.error "REST API disabled" + return httpError(403, "Access denied") + } + + if (settings.pincode && settings.armWithPin && (params.pincode != settings.pincode.toString())) { + log.error "Invalid PIN code '${params.pincode}'" + return httpError(403, "Access denied") + } + + armAway() + return apiStatus() +} + +// .../armstay REST API endpoint +def apiArmStay() { + LOG("apiArmStay()") + + if (!isRestApiEnabled()) { + log.error "REST API disabled" + return httpError(403, "Access denied") + } + + if (settings.pincode && settings.armWithPin && (params.pincode != settings.pincode.toString())) { + log.error "Invalid PIN code '${params.pincode}'" + return httpError(403, "Access denied") + } + + armStay() + return apiStatus() +} + +// .../disarm REST API endpoint +def apiDisarm() { + LOG("apiDisarm()") + + if (!isRestApiEnabled()) { + log.error "REST API disabled" + return httpError(403, "Access denied") + } + + if (settings.pincode && (params.pincode != settings.pincode.toString())) { + log.error "Invalid PIN code '${params.pincode}'" + return httpError(403, "Access denied") + } + + disarm() + return apiStatus() +} + +// .../panic REST API endpoint +def apiPanic() { + LOG("apiPanic()") + + if (!isRestApiEnabled()) { + log.error "REST API disabled" + return httpError(403, "Access denied") + } + + panic() + return apiStatus() +} + +// .../status REST API endpoint +def apiStatus() { + LOG("apiStatus()") + + if (!isRestApiEnabled()) { + log.error "REST API disabled" + return httpError(403, "Access denied") + } + + def status = [ + status: state.armed ? (state.stay ? "armed stay" : "armed away") : "disarmed", + alarms: state.alarms + ] + + return status +} + +def activateAlarm() { + LOG("activateAlarm()") + + if (state.alarms.size() == 0) { + log.warn "activateAlarm: false alarm" + return + } + + switch (settings.sirenMode) { + case "Siren": + settings.alarms*.siren() + break + + case "Strobe": + settings.alarms*.strobe() + break + + case "Both": + settings.alarms*.both() + break + } + + // Only turn on those switches that are currently off + def switchesOn = settings.switches?.findAll { it?.currentSwitch == "off" } + LOG("switchesOn: ${switchesOn}") + if (switchesOn) { + switchesOn*.on() + state.offSwitches = switchesOn.collect { it.id } + } + + settings.cameras*.take() + + if (settings.helloHomeAction) { + log.info "Executing HelloHome action '${settings.helloHomeAction}'" + location.helloHome.execute(settings.helloHomeAction) + } + + def msg = "Alarm at ${location.name}!" + state.alarms.each() { + msg += "\n${it}" + } + + notify(msg) + notifyVoice() + + myRunIn(180, reset) +} + +private def notify(msg) { + LOG("notify(${msg})") + + log.info msg + + if (state.alarms.size()) { + // Alarm notification + if (settings.pushMessage) { + mySendPush(msg) + } else { + sendNotificationEvent(msg) + } + + if (settings.smsAlarmPhone1 && settings.phone1) { + sendSms(phone1, msg) + } + + if (settings.smsAlarmPhone2 && settings.phone2) { + sendSms(phone2, msg) + } + + if (settings.smsAlarmPhone3 && settings.phone3) { + sendSms(phone3, msg) + } + + if (settings.smsAlarmPhone4 && settings.phone4) { + sendSms(phone4, msg) + } + + if (settings.pushbulletAlarm && settings.pushbullet) { + settings.pushbullet*.push(location.name, msg) + } + } else { + // Status change notification + if (settings.pushStatusMessage) { + mySendPush(msg) + } else { + sendNotificationEvent(msg) + } + + if (settings.smsStatusPhone1 && settings.phone1) { + sendSms(phone1, msg) + } + + if (settings.smsStatusPhone2 && settings.phone2) { + sendSms(phone2, msg) + } + + if (settings.smsStatusPhone3 && settings.phone3) { + sendSms(phone3, msg) + } + + if (settings.smsStatusPhone4 && settings.phone4) { + sendSms(phone4, msg) + } + + if (settings.pushbulletStatus && settings.pushbullet) { + settings.pushbullet*.push(location.name, msg) + } + } +} + +private def notifyVoice() { + LOG("notifyVoice()") + + if (!settings.audioPlayer) { + return + } + + def phrase = null + if (state.alarms.size()) { + // Alarm notification + if (settings.speechOnAlarm) { + phrase = settings.speechText ?: getStatusPhrase() + } + } else { + // Status change notification + if (settings.speechOnStatus) { + if (state.armed) { + if (state.stay) { + phrase = settings.speechTextArmedStay ?: getStatusPhrase() + } else { + phrase = settings.speechTextArmedAway ?: getStatusPhrase() + } + } else { + phrase = settings.speechTextDisarmed ?: getStatusPhrase() + } + } + } + + if (phrase) { + settings.audioPlayer*.playText(phrase) + } +} + +private def history(String event, String description = "") { + LOG("history(${event}, ${description})") + + def history = atomicState.history + history << [time: now(), event: event, description: description] + if (history.size() > 10) { + history = history.sort{it.time} + history = history[1..-1] + } + + LOG("history: ${history}") + state.history = history +} + +private def getStatusPhrase() { + LOG("getStatusPhrase()") + + def phrase = "" + if (state.alarms.size()) { + phrase = "Alarm at ${location.name}!" + state.alarms.each() { + phrase += " ${it}." + } + } else { + phrase = "${location.name} security is " + if (state.armed) { + def mode = state.stay ? "stay" : "away" + phrase += "armed in ${mode} mode." + } else { + phrase += "disarmed." + } + } + + return phrase +} + +private def getHelloHomeActions() { + def actions = location.helloHome?.getPhrases().collect() { it.label } + return actions.sort() +} + +private def getAlarmStatus() { + def alarmStatus + + if (atomicState.armed) { + alarmStatus = "ARMED " + alarmStatus += atomicState.stay ? "STAY" : "AWAY" + } else { + alarmStatus = "DISARMED" + } + + return alarmStatus +} + +private def getZoneStatus(device, sensorType) { + + def zone = getZoneForDevice(device.id, sensorType) + if (!zone) { + return null + } + + def str = "${device.displayName}: ${zone.zoneType}, " + str += zone.armed ? "armed, " : "disarmed, " + str += device.currentValue(sensorType) + + return str +} + +private def getZoneForDevice(id, sensorType) { + return state.zones.find() { it.deviceId == id && it.sensorType == sensorType } +} + +private def isZoneReady(device, sensorType) { + def ready + + switch (sensorType) { + case "contact": + ready = "closed".equals(device.currentValue("contact")) + break + + case "motion": + ready = "inactive".equals(device.currentValue("motion")) + break + + case "acceleration": + ready = "inactive".equals(device.currentValue("acceleration")) + break + + case "smoke": + ready = "clear".equals(device.currentValue("smoke")) + break + + case "water": + ready = "dry".equals(device.currentValue("water")) + break + + default: + ready = false + } + + return ready +} + +private def getDeviceById(id, sensorType) { + switch (sensorType) { + case "contact": + return settings.z_contact?.find() { it.id == id } + + case "motion": + return settings.z_motion?.find() { it.id == id } + + case "acceleration": + return settings.z_movement?.find() { it.id == id } + + case "smoke": + return settings.z_smoke?.find() { it.id == id } + + case "water": + return settings.z_water?.find() { it.id == id } + } + + return null +} + +private def getNumZones() { + def numZones = 0 + + numZones += settings.z_contact?.size() ?: 0 + numZones += settings.z_motion?.size() ?: 0 + numZones += settings.z_movement?.size() ?: 0 + numZones += settings.z_smoke?.size() ?: 0 + numZones += settings.z_water?.size() ?: 0 + + return numZones +} + +private def myRunIn(delay_s, func) { + if (delay_s > 0) { + def date = new Date(now() + (delay_s * 1000)) + runOnce(date, func) + LOG("scheduled '${func}' to run at ${date}") + } +} + +private def mySendPush(msg) { + // sendPush can throw an exception + try { + sendPush(msg) + } catch (e) { + log.error e + } +} + +private def getVersion() { + return "2.4.3" +} + +private def textCopyright() { + def text = "Copyright © 2014 Statusbits.com" +} + +private def textLicense() { + def text = + "This program is free software: you can redistribute it and/or " + + "modify it under the terms of the GNU General Public License as " + + "published by the Free Software Foundation, either version 3 of " + + "the License, or (at your option) any later version.\n\n" + + "This program is distributed in the hope that it will be useful, " + + "but WITHOUT ANY WARRANTY; without even the implied warranty of " + + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU " + + "General Public License for more details.\n\n" + + "You should have received a copy of the GNU General Public License " + + "along with this program. If not, see ." +} + +private def LOG(message) { + //log.trace message +} + +private def STATE() { + //log.trace "state: ${state}" +} diff --git a/smartapps/tslagle13/hello-home-phrase-director.src/hello-home-phrase-director.groovy b/smartapps/tslagle13/hello-home-phrase-director.src/hello-home-phrase-director.groovy new file mode 100644 index 00000000000..de520ecda10 --- /dev/null +++ b/smartapps/tslagle13/hello-home-phrase-director.src/hello-home-phrase-director.groovy @@ -0,0 +1,321 @@ + /** + * Magic Home + * + * Copyright 2014 Tim Slagle + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + definition( + name: "Hello, Home Phrase Director", + namespace: "tslagle13", + author: "Tim Slagle", + description: "Monitor a set of presence sensors and activate Hello, Home phrases based on whether your home is empty or occupied. Each presence status change will check against the current 'sun state' to run phrases based on occupancy and whether the sun is up or down.", + category: "Convenience", + iconUrl: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png", + iconX2Url: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png" + ) + + preferences { + page(name: "selectPhrases") + + page( name:"Settings", title:"Settings", uninstall:true, install:true ) { + section("False alarm threshold (defaults to 10 min)") { + input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false + } + + section("Zip code (for sunrise/sunset)") { + input "zip", "decimal", required: true + } + + section("Notifications") { + input "sendPushMessage", "enum", title: "Send a push notification when house is empty?", metadata:[values:["Yes","No"]], required:false + input "sendPushMessageHome", "enum", title: "Send a push notification when home is occupied?", metadata:[values:["Yes","No"]], required:false + } + + section(title: "More options", hidden: hideOptionsSection(), hideable: true) { + label title: "Assign a name", required: false + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + } + } + + def selectPhrases() { + def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.homeNight) + dynamicPage(name: "selectPhrases", title: "Configure", nextPage:"Settings", uninstall: true) { + section("Who?") { + input "people", "capability.presenceSensor", title: "Monitor These Presences", required: true, multiple: true, submitOnChange:true + } + + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + phrases.sort() + section("Run This Phrase When...") { + log.trace phrases + input "awayDay", "enum", title: "Everyone Is Away And It's Day", required: true, options: phrases, submitOnChange:true + input "awayNight", "enum", title: "Everyone Is Away And It's Night", required: true, options: phrases, submitOnChange:true + input "homeDay", "enum", title: "At Least One Person Is Home And It's Day", required: true, options: phrases, submitOnChange:true + input "homeNight", "enum", title: "At Least One Person Is Home And It's Night", required: true, options: phrases, submitOnChange:true + } + section("Select modes used for each condition. (Needed for better app logic)") { + input "homeModeDay", "mode", title: "Select Mode Used for 'Home Day'", required: true + input "homeModeNight", "mode", title: "Select Mode Used for 'Home Night'", required: true + } + } + } + } + + def installed() { + initialize() + + } + + def updated() { + unsubscribe() + initialize() + } + + def initialize() { + subscribe(people, "presence", presence) + runIn(60, checkSun) + subscribe(location, "sunrise", setSunrise) + subscribe(location, "sunset", setSunset) + } + + //check current sun state when installed. + def checkSun() { + def zip = settings.zip as String + def sunInfo = getSunriseAndSunset(zipCode: zip) + def current = now() + + if (sunInfo.sunrise.time < current && sunInfo.sunset.time > current) { + state.sunMode = "sunrise" + setSunrise() + } + + else { + state.sunMode = "sunset" + setSunset() + } + } + + //change to sunrise mode on sunrise event + def setSunrise(evt) { + state.sunMode = "sunrise"; + changeSunMode(newMode); + } + + //change to sunset mode on sunset event + def setSunset(evt) { + state.sunMode = "sunset"; + changeSunMode(newMode) + } + + //change mode on sun event + def changeSunMode(newMode) { + if(allOk) { + + if(everyoneIsAway() && (state.sunMode == "sunrise")) { + log.info("Home is Empty Setting New Away Mode") + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 + runIn(delay, "setAway") + } + + if(everyoneIsAway() && (state.sunMode == "sunset")) { + log.info("Home is Empty Setting New Away Mode") + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 + runIn(delay, "setAway") + } + + else { + log.info("Home is Occupied Setting New Home Mode") + setHome() + + + } + } + } + + //presence change run logic based on presence state of home + def presence(evt) { + if(allOk) { + if(evt.value == "not present") { + log.debug("Checking if everyone is away") + + if(everyoneIsAway()) { + log.info("Nobody is home, running away sequence") + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 + runIn(delay, "setAway") + } + } + + else { + def lastTime = state[evt.deviceId] + if (lastTime == null || now() - lastTime >= 1 * 60000) { + log.info("Someone is home, running home sequence") + setHome() + } + state[evt.deviceId] = now() + + } + } + } + + //if empty set home to one of the away modes + def setAway() { + if(everyoneIsAway()) { + if(state.sunMode == "sunset") { + def message = "Performing \"${awayNight}\" for you as requested." + log.info(message) + sendAway(message) + location.helloHome.execute(settings.awayNight) + } + + else if(state.sunMode == "sunrise") { + def message = "Performing \"${awayDay}\" for you as requested." + log.info(message) + sendAway(message) + location.helloHome.execute(settings.awayDay) + } + else { + log.debug("Mode is the same, not evaluating") + } + } + + else { + log.info("Somebody returned home before we set to '${newAwayMode}'") + } + } + + //set home mode when house is occupied + def setHome() { + + log.info("Setting Home Mode!!") + if(anyoneIsHome()) { + if(state.sunMode == "sunset"){ + if (location.mode != "${homeModeNight}"){ + def message = "Performing \"${homeNight}\" for you as requested." + log.info(message) + sendHome(message) + location.helloHome.execute(settings.homeNight) + } + } + + if(state.sunMode == "sunrise"){ + if (location.mode != "${homeModeDay}"){ + def message = "Performing \"${homeDay}\" for you as requested." + log.info(message) + sendHome(message) + location.helloHome.execute(settings.homeDay) + } + } + } + + } + + private everyoneIsAway() { + def result = true + + if(people.findAll { it?.currentPresence == "present" }) { + result = false + } + + log.debug("everyoneIsAway: ${result}") + + return result + } + + private anyoneIsHome() { + def result = false + + if(people.findAll { it?.currentPresence == "present" }) { + result = true + } + + log.debug("anyoneIsHome: ${result}") + + return result + } + + def sendAway(msg) { + if(sendPushMessage != "No") { + log.debug("Sending push message") + sendPush(msg) + } + + log.debug(msg) + } + + def sendHome(msg) { + if(sendPushMessageHome != "No") { + log.debug("Sending push message") + sendPush(msg) + } + + log.debug(msg) + } + + private getAllOk() { + modeOk && daysOk && timeOk + } + + private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result + } + + private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result + } + + private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result + } + + private hhmm(time, fmt = "h:mm a") + { + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) + } + + private getTimeIntervalLabel() + { + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" + } + + private hideOptionsSection() { + (starting || ending || days || modes) ? false : true + } diff --git a/smartapps/tslagle13/lighting-director.src/lighting-director.groovy b/smartapps/tslagle13/lighting-director.src/lighting-director.groovy new file mode 100644 index 00000000000..d9ddfc30275 --- /dev/null +++ b/smartapps/tslagle13/lighting-director.src/lighting-director.groovy @@ -0,0 +1,1311 @@ +/** + * Lighting Director + * + * Source: https://github.com/tslagle13/SmartThings/blob/master/Director-Series-Apps/Lighting-Director/Lighting%20Director.groovy + * + * Current Version: 2.9.4 + * + * + * Changelog: + * Version - 1.3 + * Version - 1.30.1 Modification by Michael Struck - Fixed syntax of help text and titles of scenarios, along with a new icon + * Version - 1.40.0 Modification by Michael Struck - Code optimization and added door contact sensor capability + * Version - 1.41.0 Modification by Michael Struck - Code optimization and added time restrictions to each scenario + * Version - 2.0 Tim Slagle - Moved to only have 4 slots. Code was to heavy and needed to be trimmed. + * Version - 2.1 Tim Slagle - Moved time interval inputs inline with STs design. + * Version - 2.2 Michael Struck - Added the ability to activate switches via the status locks and fixed some syntax issues + * Version - 2.5 Michael Struck - Changed the way the app unschedules re-triggered events + * Version - 2.5.1 Tim Slagle - Fixed Time Logic + * Version - 2.6 Michael Struck - Added the additional restriction of running triggers once per day and misc cleanup of code + * Version - 2.7 Michael Struck - Added feature that turns off triggering if the physical switch is pressed. + * Version - 2.81 Michael Struck - Fixed an issue with dimmers not stopping light action + * Version - 2.9 Michael Struck - Fixed issue where button presses outside of the time restrictions prevent the triggers from firing and code optimization + * Version - 2.9.1 Tim Slagle - Further enhanced time interval logic. + * Version - 2.9.2 Brandon Gordon - Added support for acceleration sensors. + * Version - 2.9.3 Brandon Gordon - Added mode change subscriptions. + * Version - 2.9.4 Michael Struck - Code Optimization when triggers are tripped + * +* Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy + * + * Copyright 2015 Tim Slagle and Michael Struck + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Lighting Director", + namespace: "tslagle13", + author: "Tim Slagle & Michael Struck", + description: "Control up to 4 sets (scenarios) of lights based on motion, door contacts and illuminance levels.", + category: "Convenience", + iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector.png", + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector@2x.png", + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Lighting-Director/LightingDirector@2x.png") + +preferences { + page name:"pageSetup" + page name:"pageSetupScenarioA" + page name:"pageSetupScenarioB" + page name:"pageSetupScenarioC" + page name:"pageSetupScenarioD" +} + +// Show setup page +def pageSetup() { + + def pageProperties = [ + name: "pageSetup", + nextPage: null, + install: true, + uninstall: true + ] + + return dynamicPage(pageProperties) { + section("Setup Menu") { + href "pageSetupScenarioA", title: getTitle(settings.ScenarioNameA), description: getDesc(settings.ScenarioNameA), state: greyOut(settings.ScenarioNameA) + href "pageSetupScenarioB", title: getTitle(settings.ScenarioNameB), description: getDesc(settings.ScenarioNameB), state: greyOut(settings.ScenarioNameB) + href "pageSetupScenarioC", title: getTitle(settings.ScenarioNameC), description: getDesc(settings.ScenarioNameC), state: greyOut(settings.ScenarioNameC) + href "pageSetupScenarioD", title: getTitle(settings.ScenarioNameD), description: getDesc(settings.ScenarioNameD), state: greyOut(settings.ScenarioNameD) + } + section([title:"Options", mobileOnly:true]) { + label title:"Assign a name", required:false + } + } +} + +// Show "pageSetupScenarioA" page +def pageSetupScenarioA() { + + def inputLightsA = [ + name: "A_switches", + type: "capability.switch", + title: "Control the following switches...", + multiple: true, + required: false + ] + def inputDimmersA = [ + name: "A_dimmers", + type: "capability.switchLevel", + title: "Dim the following...", + multiple: true, + required: false + ] + + def inputMotionA = [ + name: "A_motion", + type: "capability.motionSensor", + title: "Using these motion sensors...", + multiple: true, + required: false + ] + + def inputAccelerationA = [ + name: "A_acceleration", + type: "capability.accelerationSensor", + title: "Or using these acceleration sensors...", + multiple: true, + required: false + ] + def inputContactA = [ + name: "A_contact", + type: "capability.contactSensor", + title: "Or using these contact sensors...", + multiple: true, + required: false + ] + + def inputTriggerOnceA = [ + name: "A_triggerOnce", + type: "bool", + title: "Trigger only once per day...", + defaultValue:false + ] + + def inputSwitchDisableA = [ + name: "A_switchDisable", + type: "bool", + title: "Stop triggering if physical switches/dimmers are turned off...", + defaultValue:false + ] + + def inputLockA = [ + name: "A_lock", + type: "capability.lock", + title: "Or using these locks...", + multiple: true, + required: false + ] + + def inputModeA = [ + name: "A_mode", + type: "mode", + title: "Only during the following modes...", + multiple: true, + required: false + ] + + def inputDayA = [ + name: "A_day", + type: "enum", + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + title: "Only on certain days of the week...", + multiple: true, + required: false + ] + + + def inputLevelA = [ + name: "A_level", + type: "enum", + options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]], + title: "Set dimmers to this level", + multiple: false, + required: false + ] + + def inputTurnOnLuxA = [ + name: "A_turnOnLux", + type: "number", + title: "Only run this scenario if lux is below...", + multiple: false, + required: false + ] + + def inputLuxSensorsA = [ + name: "A_luxSensors", + type: "capability.illuminanceMeasurement", + title: "On these lux sensors", + multiple: false, + required: false + ] + + def inputTurnOffA = [ + name: "A_turnOff", + type: "number", + title: "Turn off this scenario after motion stops or doors close/lock (minutes)...", + multiple: false, + required: false + ] + + def inputScenarioNameA = [ + name: "ScenarioNameA", + type: "text", + title: "Scenario Name", + multiple: false, + required: false, + defaultValue: empty + ] + + def pageProperties = [ + name: "pageSetupScenarioA", + ] + + return dynamicPage(pageProperties) { +section("Name your scenario") { + input inputScenarioNameA + } + +section("Devices included in the scenario") { + input inputMotionA + input inputAccelerationA + input inputContactA + input inputLockA + input inputLightsA + input inputDimmersA + } + +section("Scenario settings") { + input inputLevelA + input inputTurnOnLuxA + input inputLuxSensorsA + input inputTurnOffA + } + +section("Scenario restrictions") { + input inputTriggerOnceA + input inputSwitchDisableA + href "timeIntervalInputA", title: "Only during a certain time...", description: getTimeLabel(A_timeStart, A_timeEnd), state: greyedOutTime(A_timeStart, A_timeEnd), refreshAfterSelection:true + input inputDayA + input inputModeA + } + +section("Help") { + paragraph helpText() + } + } + +} + +def pageSetupScenarioB() { + + def inputLightsB = [ + name: "B_switches", + type: "capability.switch", + title: "Control the following switches...", + multiple: true, + required: false + ] + def inputDimmersB = [ + name: "B_dimmers", + type: "capability.switchLevel", + title: "Dim the following...", + multiple: true, + required: false + ] + + def inputTurnOnLuxB = [ + name: "B_turnOnLux", + type: "number", + title: "Only run this scenario if lux is below...", + multiple: false, + required: false + ] + + def inputLuxSensorsB = [ + name: "B_luxSensors", + type: "capability.illuminanceMeasurement", + title: "On these lux sensors", + multiple: false, + required: false + ] + + def inputMotionB = [ + name: "B_motion", + type: "capability.motionSensor", + title: "Using these motion sensors...", + multiple: true, + required: false + ] + + def inputAccelerationB = [ + name: "B_acceleration", + type: "capability.accelerationSensor", + title: "Or using these acceleration sensors...", + multiple: true, + required: false + ] + def inputContactB = [ + name: "B_contact", + type: "capability.contactSensor", + title: "Or using these contact sensors...", + multiple: true, + required: false + ] + + def inputTriggerOnceB = [ + name: "B_triggerOnce", + type: "bool", + title: "Trigger only once per day...", + defaultValue:false + ] + + def inputSwitchDisableB = [ + name: "B_switchDisable", + type: "bool", + title: "Stop triggering if physical switches/dimmers are turned off...", + defaultValue:false + ] + + def inputLockB = [ + name: "B_lock", + type: "capability.lock", + title: "Or using these locks...", + multiple: true, + required: false + ] + + def inputModeB = [ + name: "B_mode", + type: "mode", + title: "Only during the following modes...", + multiple: true, + required: false + ] + + def inputDayB = [ + name: "B_day", + type: "enum", + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + title: "Only on certain days of the week...", + multiple: true, + required: false + ] + + def inputLevelB = [ + name: "B_level", + type: "enum", + options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]], + title: "Set dimmers to this level", + multiple: false, + required: false + ] + + def inputTurnOffB = [ + name: "B_turnOff", + type: "number", + title: "Turn off this scenario after motion stops or doors close/lock (minutes)...", + multiple: false, + required: false + ] + + def inputScenarioNameB = [ + name: "ScenarioNameB", + type: "text", + title: "Scenario Name", + multiple: false, + required: false, + defaultValue: empty + ] + + def pageProperties = [ + name: "pageSetupScenarioB", + ] + + return dynamicPage(pageProperties) { +section("Name your scenario") { + input inputScenarioNameB + } + +section("Devices included in the scenario") { + input inputMotionB + input inputAccelerationB + input inputContactB + input inputLockB + input inputLightsB + input inputDimmersB + } + +section("Scenario settings") { + input inputLevelB + input inputTurnOnLuxB + input inputLuxSensorsB + input inputTurnOffB + } + +section("Scenario restrictions") { + input inputTriggerOnceB + input inputSwitchDisableB + href "timeIntervalInputB", title: "Only during a certain time...", description: getTimeLabel(B_timeStart, B_timeEnd), state: greyedOutTime(B_timeStart, B_timeEnd), refreshAfterSelection:true + input inputDayB + input inputModeB + } + +section("Help") { + paragraph helpText() + } + } +} + +def pageSetupScenarioC() { + + def inputLightsC = [ + name: "C_switches", + type: "capability.switch", + title: "Control the following switches...", + multiple: true, + required: false + ] + def inputDimmersC = [ + name: "C_dimmers", + type: "capability.switchLevel", + title: "Dim the following...", + multiple: true, + required: false + ] + + def inputMotionC = [ + name: "C_motion", + type: "capability.motionSensor", + title: "Using these motion sensors...", + multiple: true, + required: false + ] + + def inputAccelerationC = [ + name: "C_acceleration", + type: "capability.accelerationSensor", + title: "Or using these acceleration sensors...", + multiple: true, + required: false + ] + def inputContactC = [ + name: "C_contact", + type: "capability.contactSensor", + title: "Or using these contact sensors...", + multiple: true, + required: false + ] + + def inputTriggerOnceC = [ + name: "C_triggerOnce", + type: "bool", + title: "Trigger only once per day...", + defaultValue:false + ] + + def inputSwitchDisableC = [ + name: "C_switchDisable", + type: "bool", + title: "Stop triggering if physical switches/dimmers are turned off...", + defaultValue:false + ] + + def inputLockC = [ + name: "C_lock", + type: "capability.lock", + title: "Or using these locks...", + multiple: true, + required: false + ] + + def inputModeC = [ + name: "C_mode", + type: "mode", + title: "Only during the following modes...", + multiple: true, + required: false + ] + + def inputDayC = [ + name: "C_day", + type: "enum", + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + title: "Only on certain days of the week...", + multiple: true, + required: false + ] + + def inputLevelC = [ + name: "C_level", + type: "enum", + options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]], + title: "Set dimmers to this level", + multiple: false, + required: false + ] + + def inputTurnOffC = [ + name: "C_turnOff", + type: "number", + title: "Turn off this scenario after motion stops or doors close/lock (minutes)...", + multiple: false, + required: false + ] + + def inputScenarioNameC = [ + name: "ScenarioNameC", + type: "text", + title: "Scenario Name", + multiple: false, + required: false, + defaultValue: empty + ] + + def inputTurnOnLuxC = [ + name: "C_turnOnLux", + type: "number", + title: "Only run this scenario if lux is below...", + multiple: false, + required: false + ] + + def inputLuxSensorsC = [ + name: "C_luxSensors", + type: "capability.illuminanceMeasurement", + title: "On these lux sensors", + multiple: false, + required: false + ] + + def pageProperties = [ + name: "pageSetupScenarioC", + ] + + return dynamicPage(pageProperties) { + section("Name your scenario") { + input inputScenarioNameC + } + +section("Devices included in the scenario") { + input inputMotionC + input inputAccelerationC + input inputContactC + input inputLockC + input inputLightsC + input inputDimmersC + } + +section("Scenario settings") { + input inputLevelC + input inputTurnOnLuxC + input inputLuxSensorsC + input inputTurnOffC + } + +section("Scenario restrictions") { + input inputTriggerOnceC + input inputSwitchDisableC + href "timeIntervalInputC", title: "Only during a certain time...", description: getTimeLabel(C_timeStart, C_timeEnd), state: greyedOutTime(C_timeStart, C_timeEnd), refreshAfterSelection:true + input inputDayC + input inputModeC + } + +section("Help") { + paragraph helpText() + } + } +} + +def pageSetupScenarioD() { + + def inputLightsD = [ + name: "D_switches", + type: "capability.switch", + title: "Control the following switches...", + multiple: true, + required: false + ] + def inputDimmersD = [ + name: "D_dimmers", + type: "capability.switchLevel", + title: "Dim the following...", + multiple: true, + required: false + ] + + def inputMotionD = [ + name: "D_motion", + type: "capability.motionSensor", + title: "Using these motion sensors...", + multiple: true, + required: false + ] + + def inputAccelerationD = [ + name: "D_acceleration", + type: "capability.accelerationSensor", + title: "Or using these acceleration sensors...", + multiple: true, + required: false + ] + def inputContactD = [ + name: "D_contact", + type: "capability.contactSensor", + title: "Or using these contact sensors...", + multiple: true, + required: false + ] + + def inputLockD = [ + name: "D_lock", + type: "capability.lock", + title: "Or using these locks...", + multiple: true, + required: false + ] + + def inputModeD = [ + name: "D_mode", + type: "mode", + title: "Only during the following modes...", + multiple: true, + required: false + ] + + def inputTriggerOnceD = [ + name: "D_triggerOnce", + type: "bool", + title: "Trigger only once per day...", + defaultValue:false + ] + + def inputSwitchDisableD = [ + name: "D_switchDisable", + type: "bool", + title: "Stop triggering if physical switches/dimmers are turned off...", + defaultValue:false + ] + + def inputDayD = [ + name: "D_day", + type: "enum", + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + title: "Only on certain days of the week...", + multiple: true, + required: false + ] + + + def inputLevelD = [ + name: "D_level", + type: "enum", + options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]], + title: "Set dimmers to this level", + multiple: false, + required: false + ] + + def inputTurnOffD = [ + name: "D_turnOff", + type: "number", + title: "Turn off this scenario after motion stops, doors close or close/lock (minutes)...", + multiple: false, + required: false + ] + + def inputScenarioNameD = [ + name: "ScenarioNameD", + type: "text", + title: "Scenario Name", + multiple: false, + required: false, + defaultValue: empty + ] + + def inputTurnOnLuxD = [ + name: "D_turnOnLux", + type: "number", + title: "Only run this scenario if lux is below...", + multiple: false, + required: false + ] + + def inputLuxSensorsD = [ + name: "D_luxSensors", + type: "capability.illuminanceMeasurement", + title: "On these lux sensors", + multiple: false, + required: false + ] + + def pageProperties = [ + name: "pageSetupScenarioD", + ] + + return dynamicPage(pageProperties) { + section("Name your scenario") { + input inputScenarioNameD + } + +section("Devices included in the scenario") { + input inputMotionD + input inputAccelerationD + input inputContactD + input inputLockD + input inputLightsD + input inputDimmersD + } + +section("Scenario settings") { + input inputLevelD + input inputTurnOnLuxD + input inputLuxSensorsD + input inputTurnOffD + } + +section("Scenario restrictions") { + input inputTriggerOnceD + input inputSwitchDisableD + href "timeIntervalInputD", title: "Only during a certain time", description: getTimeLabel(D_timeStart, D_timeEnd), state: greyedOutTime(D_timeStart, D_timeEnd), refreshAfterSelection:true + input inputDayD + input inputModeD + } + +section("Help") { + paragraph helpText() + } + } +} + +def installed() { + initialize() +} + +def updated() { + unschedule() + unsubscribe() + initialize() +} + +def initialize() { + +midNightReset() + +if(A_motion) { + subscribe(settings.A_motion, "motion", onEventA) +} + +if(A_acceleration) { + subscribe(settings.A_acceleration, "acceleration", onEventA) +} + +if(A_contact) { + subscribe(settings.A_contact, "contact", onEventA) +} + +if(A_lock) { + subscribe(settings.A_lock, "lock", onEventA) +} + +if(A_switchDisable) { + subscribe(A_switches, "switch.off", onPressA) + subscribe(A_dimmers, "switch.off", onPressA) +} + +if(A_mode) { + subscribe(location, onEventA) +} + +if(B_motion) { + subscribe(settings.B_motion, "motion", onEventB) +} + +if(B_acceleration) { + subscribe(settings.B_acceleration, "acceleration", onEventB) +} + +if(B_contact) { + subscribe(settings.B_contact, "contact", onEventB) +} + +if(B_lock) { + subscribe(settings.B_lock, "lock", onEventB) +} + +if(B_switchDisable) { + subscribe(B_switches, "switch.off", onPressB) + subscribe(B_dimmers, "switch.off", onPressB) +} + +if(B_mode) { + subscribe(location, onEventB) +} + +if(C_motion) { + subscribe(settings.C_motion, "motion", onEventC) +} + +if(C_acceleration) { + subscribe(settings.C_acceleration, "acceleration", onEventC) +} + +if(C_contact) { + subscribe(settings.C_contact, "contact", onEventC) +} + +if(C_lock) { + subscribe(settings.C_lock, "lock", onEventC) +} + +if(C_switchDisable) { + subscribe(C_switches, "switch.off", onPressC) + subscribe(C_dimmers, "switch.off", onPressC) +} + +if(C_mode) { + subscribe(location, onEventC) +} + +if(D_motion) { + subscribe(settings.D_motion, "motion", onEventD) +} + +if(D_acceleration) { + subscribe(settings.D_acceleration, "acceleration", onEventD) +} + +if(D_contact) { + subscribe(settings.D_contact, "contact", onEventD) +} + +if(D_lock) { + subscribe(settings.D_lock, "lock", onEventD) +} + +if(D_switchDisable) { + subscribe(D_switches, "switch.off", onPressD) + subscribe(D_dimmers, "switch.off", onPressD) +} + +if(D_mode) { + subscribe(location, onEventD) +} + +} + +def onEventA(evt) { + +if ((!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))) { //Checks to make sure this scenario should be triggered more then once in a day +if ((!A_mode || A_mode.contains(location.mode)) && getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)) { //checks to make sure we are not opperating outside of set restrictions. +if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){ //checks to make sure illimunance is either not cared about or if the value is within the restrictions +def A_levelOn = A_level as Integer + +//Check states of each device to see if they are to be ignored or if they meet the requirments of the app to produce an action. +if (getInputOk(A_motion, A_contact, A_lock, A_acceleration)) { + log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameA}'") + settings.A_dimmers?.setLevel(A_levelOn) + settings.A_switches?.on() + if (A_triggerOnce){ + state.A_triggered = true + if (!A_turnOff) { + runOnce (getMidnight(), midNightReset) + } + } + if (state.A_timerStart){ + unschedule(delayTurnOffA) + state.A_timerStart = false + } +} + +//if none of the above paramenters meet the expectation of the app then turn off +else { + + if (settings.A_turnOff) { + runIn(A_turnOff * 60, "delayTurnOffA") + state.A_timerStart = true + } + else { + settings.A_switches?.off() + settings.A_dimmers?.setLevel(0) + if (state.A_triggered) { + runOnce (getMidnight(), midNightReset) + } + } +} +} +} +else{ +log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction. Not running scenario.") +} +} +} + +def delayTurnOffA(){ + settings.A_switches?.off() + settings.A_dimmers?.setLevel(0) + state.A_timerStart = false + if (state.A_triggered) { + runOnce (getMidnight(), midNightReset) + } + +} + +//when physical switch is actuated disable the scenario +def onPressA(evt) { +if ((!A_mode || A_mode.contains(location.mode)) && getTimeOk (A_timeStart, A_timeEnd) && getDayOk(A_day)) { //checks to make sure we are not opperating outside of set restrictions. +if ((!A_luxSensors) || (A_luxSensors.latestValue("illuminance") <= A_turnOnLux)){ +if ((!A_triggerOnce || (A_triggerOnce && !state.A_triggered)) && (!A_switchDisable || (A_switchDisable && !state.A_triggered))) { + if (evt.physical){ + state.A_triggered = true + unschedule(delayTurnOffA) + runOnce (getMidnight(), midNightReset) + log.debug "Physical switch in '${ScenarioNameA}' pressed. Triggers for this scenario disabled." + } +} +}}} + +def onEventB(evt) { + +if ((!B_triggerOnce || (B_triggerOnce && !state.B_triggered)) && (!B_switchDisable || (B_switchDisable && !state.B_triggered))) { //Checks to make sure this scenario should be triggered more then once in a day +if ((!B_mode ||B_mode.contains(location.mode)) && getTimeOk (B_timeStart, B_timeEnd) && getDayOk(B_day)) { //checks to make sure we are not opperating outside of set restrictions. +if ((!B_luxSensors) || (B_luxSensors.latestValue("illuminance") <= B_turnOnLux)) { //checks to make sure illimunance is either not cared about or if the value is within the restrictions +def B_levelOn = B_level as Integer + +//Check states of each device to see if they are to be ignored or if they meet the requirments of the app to produce an action. +if (getInputOk(B_motion, B_contact, B_lock, B_acceleration)) { + + log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameB}'") + settings.B_dimmers?.setLevel(B_levelOn) + settings.B_switches?.on() + if (B_triggerOnce){ + state.B_triggered = true + if (!B_turnOff) { + runOnce (getMidnight(), midNightReset) + } + } + if (state.B_timerStart) { + unschedule(delayTurnOffB) + state.B_timerStart = false + } +} + +//if none of the above paramenters meet the expectation of the app then turn off +else { + if (settings.B_turnOff) { + runIn(B_turnOff * 60, "delayTurnOffB") + state.B_timerStart = true + } + + else { + settings.B_switches?.off() + settings.B_dimmers?.setLevel(0) + if (state.B_triggered) { + runOnce (getMidnight(), midNightReset) + } + } + +} +} +} +else{ +log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction. Not running scenario.") +} +} +} + +def delayTurnOffB(){ + settings.B_switches?.off() + settings.B_dimmers?.setLevel(0) + state.B_timerStart = false + if (state.B_triggered) { + runOnce (getMidnight(), midNightReset) + } +} + +//when physical switch is actuated disable the scenario +def onPressB(evt) { +if ((!B_mode ||B_mode.contains(location.mode)) && getTimeOk (B_timeStart, B_timeEnd) && getDayOk(B_day)) { //checks to make sure we are not opperating outside of set restrictions. +if ((!B_luxSensors) || (B_luxSensors.latestValue("illuminance") <= B_turnOnLux)) { +if ((!B_triggerOnce || (B_triggerOnce && !state.B_triggered)) && (!B_switchDisable || (B_switchDisable && !state.B_triggered))) { + if (evt.physical){ + state.B_triggered = true + unschedule(delayTurnOffB) + runOnce (getMidnight(), midNightReset) + log.debug "Physical switch in '${ScenarioNameB}' pressed. Triggers for this scenario disabled." + } +} +}}} + +def onEventC(evt) { + +if ((!C_triggerOnce || (C_triggerOnce && !state.C_triggered)) && (!C_switchDisable || (C_switchDisable && !state.C_triggered))) { //Checks to make sure this scenario should be triggered more then once in a day +if ((!C_mode || C_mode.contains(location.mode)) && getTimeOk (C_timeStart, C_timeEnd) && getDayOk(C_day) && !state.C_triggered){ //checks to make sure we are not opperating outside of set restrictions. +if ((!C_luxSensors) || (C_luxSensors.latestValue("illuminance") <= C_turnOnLux)){ //checks to make sure illimunance is either not cared about or if the value is within the restrictions +def C_levelOn = settings.C_level as Integer + +//Check states of each device to see if they are to be ignored or if they meet the requirments of the app to produce an action. +if (getInputOk(C_motion, C_contact, C_lock, C_acceleration)) { + log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameC}'") + settings.C_dimmers?.setLevel(C_levelOn) + settings.C_switches?.on() + if (C_triggerOnce){ + state.C_triggered = true + if (!C_turnOff) { + runOnce (getMidnight(), midNightReset) + } + } + if (state.C_timerStart){ + unschedule(delayTurnOffC) + state.C_timerStart = false + } +} + +//if none of the above paramenters meet the expectation of the app then turn off +else { + if (settings.C_turnOff) { + runIn(C_turnOff * 60, "delayTurnOffC") + state.C_timerStart = true + } + else { + settings.C_switches?.off() + settings.C_dimmers?.setLevel(0) + if (state.C_triggered) { + runOnce (getMidnight(), midNightReset) + } + } + +} +} +} +else{ +log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction. Not running scenario.") +} +} +} + +def delayTurnOffC(){ + settings.C_switches?.off() + settings.C_dimmers?.setLevel(0) + state.C_timerStart = false + if (state.C_triggered) { + runOnce (getMidnight(), midNightReset) + } + +} + +//when physical switch is actuated disable the scenario +def onPressC(evt) { +if ((!C_mode || C_mode.contains(location.mode)) && getTimeOk (C_timeStart, C_timeEnd) && getDayOk(C_day) && !state.C_triggered){ +if ((!C_luxSensors) || (C_luxSensors.latestValue("illuminance") <= C_turnOnLux)){ +if ((!C_triggerOnce || (C_triggerOnce && !state.C_triggered)) && (!C_switchDisable || (C_switchDisable && !state.C_triggered))) { + if (evt.physical){ + state.C_triggered = true + unschedule(delayTurnOffC) + runOnce (getMidnight(), midNightReset) + log.debug "Physical switch in '${ScenarioNameC}' pressed. Triggers for this scenario disabled." + } +} +}}} + +def onEventD(evt) { + +if ((!D_triggerOnce || (D_triggerOnce && !state.D_triggered)) && (!D_switchDisable || (D_switchDisable && !state.D_triggered))) { //Checks to make sure this scenario should be triggered more then once in a day +if ((!D_mode || D_mode.contains(location.mode)) && getTimeOk (D_timeStart, D_timeEnd) && getDayOk(D_day) && !state.D_triggered){ //checks to make sure we are not opperating outside of set restrictions. +if ((!D_luxSensors) || (D_luxSensors.latestValue("illuminance") <= D_turnOnLux)){ //checks to make sure illimunance is either not cared about or if the value is within the restrictions +def D_levelOn = D_level as Integer + +//Check states of each device to see if they are to be ignored or if they meet the requirments of the app to produce an action. +if (getInputOk(D_motion, D_contact, D_lock, D_acceleration)) { + log.debug("Motion, Door Open or Unlock Detected Running '${ScenarioNameD}'") + settings.D_dimmers?.setLevel(D_levelOn) + settings.D_switches?.on() + if (D_triggerOnce){ + state.D_triggered = true + if (!D_turnOff) { + runOnce (getMidnight(), midNightReset) + } + } + if (state.D_timerStart){ + unschedule(delayTurnOffD) + state.D_timerStart = false + } +} + +//if none of the above paramenters meet the expectation of the app then turn off +else { + if (settings.D_turnOff) { + runIn(D_turnOff * 60, "delayTurnOffD") + state.D_timerStart = true + } + else { + settings.D_switches?.off() + settings.D_dimmers?.setLevel(0) + if (state.D_triggered) { + runOnce (getMidnight(), midNightReset) + } + } +} +} +} +else{ +log.debug("Motion, Contact or Unlock detected outside of mode or time/day restriction. Not running scenario.") +} +} +} + +def delayTurnOffD(){ + settings.D_switches?.off() + settings.D_dimmers?.setLevel(0) + state.D_timerStart = false + if (state.D_triggered) { + runOnce (getMidnight(), midNightReset) + } + +} + +//when physical switch is actuated disable the scenario +def onPressD(evt) { +if ((!D_mode || D_mode.contains(location.mode)) && getTimeOk (D_timeStart, D_timeEnd) && getDayOk(D_day) && !state.D_triggered){ //checks to make sure we are not opperating outside of set restrictions. +if ((!D_luxSensors) || (D_luxSensors.latestValue("illuminance") <= D_turnOnLux)){ +if ((!D_triggerOnce || (D_triggerOnce && !state.D_triggered)) && (!D_switchDisable || (D_switchDisable && !state.D_triggered))) { + if (evt.physical){ + state.D_triggered = true + unschedule(delayTurnOffD) + runOnce (getMidnight(), midNightReset) + log.debug "Physical switch in '${ScenarioNameD}' pressed. Triggers for this scenario disabled." + } +} +}}} + +//Common Methods + +//resets once a day trigger at midnight so trigger can be ran again the next day. +def midNightReset() { + state.A_triggered = false + state.B_triggered = false + state.C_triggered = false + state.D_triggered = false +} + +private def helpText() { + def text = + "Select motion sensors, acceleration sensors, contact sensors or locks to control a set of lights. " + + "Each scenario can control dimmers and switches but can also be " + + "restricted to modes or between certain times and turned off after " + + "motion stops, doors close or lock. Scenarios can also be limited to " + + "running once or to stop running if the physical switches are turned off." + text +} + +//should scenario be marked complete or not +def greyOut(scenario){ + def result = "" + if (scenario) { + result = "complete" + } + result +} + +//should i mark the time restriction green or grey +def greyedOutTime(start, end){ + def result = "" + if (start || end) { + result = "complete" + } + result +} + + +def getTitle(scenario) { + def title = "Empty" + if (scenario) { + title = scenario + } + title +} + +//recursively applies label to each scenario depending on if the scenario has deatils inside it or not +def getDesc(scenario) { + def desc = "Tap to create a scenario" + if (scenario) { + desc = "Tap to edit scenario" + } + desc +} + + +def getMidnight() { + def midnightToday = timeToday("2000-01-01T23:59:59.999-0000", location.timeZone) + midnightToday +} + +//used to recursively check device states when methods are triggered +private getInputOk(motion, contact, lock, acceleration) { + +def motionDetected = false +def accelerationDetected = false +def contactDetected = false +def unlockDetected = false +def result = false + +if (motion) { + if (motion.latestValue("motion").contains("active")) { + motionDetected = true + } +} + +if (acceleration) { + if (acceleration.latestValue("acceleration").contains("active")) { + accelerationDetected = true + } +} + +if (contact) { + if (contact.latestValue("contact").contains("open")) { + contactDetected = true + } +} + +if (lock) { + if (lock.latestValue("lock").contains("unlocked")) { + unlockDetected = true + } +} + +result = motionDetected || contactDetected || unlockDetected || accelerationDetected +result + +} + +private getTimeOk(starting, ending) { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + + else if (starting){ + result = currTime >= start + } + else if (ending){ + result = currTime <= stop + } + + log.trace "timeOk = $result" + result +} + +def getTimeLabel(start, end){ + def timeLabel = "Tap to set" + + if(start && end){ + timeLabel = "Between" + " " + hhmm(start) + " " + "and" + " " + hhmm(end) + } + else if (start) { + timeLabel = "Start at" + " " + hhmm(start) + } + else if(end){ + timeLabel = "End at" + hhmm(end) + } + timeLabel +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +private getDayOk(dayList) { + def result = true + if (dayList) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = dayList.contains(day) + } + result +} + + +page(name: "timeIntervalInputA", title: "Only during a certain time", refreshAfterSelection:true) { + section { + input "A_timeStart", "time", title: "Starting", required: false, refreshAfterSelection:true + input "A_timeEnd", "time", title: "Ending", required: false, refreshAfterSelection:true + } + } +page(name: "timeIntervalInputB", title: "Only during a certain time", refreshAfterSelection:true) { + section { + input "B_timeStart", "time", title: "Starting", required: false, refreshAfterSelection:true + input "B_timeEnd", "time", title: "Ending", required: false, refreshAfterSelection:true + } + } +page(name: "timeIntervalInputC", title: "Only during a certain time", refreshAfterSelection:true) { + section { + input "C_timeStart", "time", title: "Starting", required: false, refreshAfterSelection:true + input "C_timeEnd", "time", title: "Ending", required: false, refreshAfterSelection:true + } + } +page(name: "timeIntervalInputD", title: "Only during a certain time", refreshAfterSelection:true) { + section { + input "D_timeStart", "time", title: "Starting", required: false, refreshAfterSelection:true + input "D_timeEnd", "time", title: "Ending", required: false, refreshAfterSelection:true + } + } \ No newline at end of file diff --git a/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy b/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy new file mode 100644 index 00000000000..8ac9d014846 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy @@ -0,0 +1,643 @@ +/** + * Thermostat Mode Director + * Source: https://github.com/tslagle13/SmartThings/blob/master/Director-Series-Apps/Thermostat-Mode-Director/Thermostat%20Mode%20Director.groovy + * + * Version 3.0 + * + * Changelog: + * 2015-05-25 + * --Updated UI to make it look pretty. + * 2015-06-01 + * --Added option for modes to trigger thermostat boost. + * + * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy + * + * Copyright 2015 Tim Slagle + * + * 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. + * + */ + +// Automatically generated. Make future change here. +definition( + name: "Thermostat Mode Director", + namespace: "tslagle13", + author: "Tim Slagle", + description: "Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.", + category: "Green Living", + iconUrl: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png", + iconX2Url: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png" +) + +preferences { + page name:"pageSetup" + page name:"directorSettings" + page name:"ThermostatandDoors" + page name:"ThermostatBoost" + page name:"Settings" + +} + +// Show setup page +def pageSetup() { + + def pageProperties = [ + name: "pageSetup", + title: "Status", + nextPage: null, + install: true, + uninstall: true + ] + + return dynamicPage(pageProperties) { + section("About 'Thermostat Mode Director'"){ + paragraph "Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open." + } + section("Setup Menu") { + href "directorSettings", title: "Director Settings", description: "", state:greyedOut() + href "ThermostatandDoors", title: "Thermostat and Doors", description: "", state: greyedOutTherm() + href "ThermostatBoost", title: "Thermostat Boost", description: "", state: greyedOutTherm1() + href "Settings", title: "Settings", description: "", state: greyedOutSettings() + } + section([title:"Options", mobileOnly:true]) { + label title:"Assign a name", required:false + } + } +} + +// Show "Setup" page +def directorSettings() { + + def sensor = [ + name: "sensor", + type: "capability.temperatureMeasurement", + title: "Which?", + multiple: false, + required: true + ] + def setLow = [ + name: "setLow", + type: "decimal", + title: "Low temp?", + required: true + ] + + def cold = [ + name: "cold", + type: "enum", + title: "Mode?", + metadata: [values:["auto", "heat", "cool", "off"]] + ] + + def setHigh = [ + name: "setHigh", + type: "decimal", + title: "High temp?", + required: true + ] + + def hot = [ + name: "hot", + type: "enum", + title: "Mode?", + metadata: [values:["auto", "heat", "cool", "off"]] + ] + + def neutral = [ + name: "neutral", + type: "enum", + title: "Mode?", + metadata: [values:["auto", "heat", "cool", "off"]] + ] + + def pageName = "Setup" + + def pageProperties = [ + name: "directorSettings", + title: "Setup", + nextPage: "pageSetup" + ] + + return dynamicPage(pageProperties) { + + section("Which temperature sensor will control your thermostat?"){ + input sensor + } + section(""){ + paragraph "Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat." + } + section("When the temperature falls below this tempurature set mode to..."){ + input setLow + input cold + } + section("When the temperature goes above this tempurature set mode to..."){ + input setHigh + input hot + } + section("When temperature is between the previous temperatures, change mode to..."){ + input neutral + } + } + +} + +def ThermostatandDoors() { + + def thermostat = [ + name: "thermostat", + type: "capability.thermostat", + title: "Which?", + multiple: true, + required: true + ] + def doors = [ + name: "doors", + type: "capability.contactSensor", + title: "Low temp?", + multiple: true, + required: true + ] + + def turnOffDelay = [ + name: "turnOffDelay", + type: "decimal", + title: "Number of minutes", + required: false + ] + + def pageName = "Thermostat and Doors" + + def pageProperties = [ + name: "ThermostatandDoors", + title: "Thermostat and Doors", + nextPage: "pageSetup" + ] + + return dynamicPage(pageProperties) { + + section(""){ + paragraph "If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)" + } + section("Choose thermostat...") { + input thermostat + } + section("If these doors/windows are open turn off thermostat regardless of outdoor temperature") { + input doors + } + section("Wait this long before turning the thermostat off (defaults to 1 minute)") { + input turnOffDelay + } + } + +} + +def ThermostatBoost() { + + def thermostat1 = [ + name: "thermostat1", + type: "capability.thermostat", + title: "Which?", + multiple: true, + required: true + ] + def turnOnTherm = [ + name: "turnOnTherm", + type: "enum", + metadata: [values: ["cool", "heat"]], + required: false + ] + + def modes1 = [ + name: "modes1", + type: "mode", + title: "Put thermostat into boost mode when mode is...", + multiple: true, + required: false + ] + + def coolingTemp = [ + name: "coolingTemp", + type: "decimal", + title: "Cooling Temp?", + required: false + ] + + def heatingTemp = [ + name: "heatingTemp", + type: "decimal", + title: "Heating Temp?", + required: false + ] + + def turnOffDelay2 = [ + name: "turnOffDelay2", + type: "decimal", + title: "Number of minutes", + required: false, + defaultValue:30 + ] + + def pageName = "Thermostat Boost" + + def pageProperties = [ + name: "ThermostatBoost", + title: "Thermostat Boost", + nextPage: "pageSetup" + ] + + return dynamicPage(pageProperties) { + + section(""){ + paragraph "Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off'" + + " and you need to heat or cool your your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat." + } + section("Choose a thermostats to boost") { + input thermostat1 + } + section("If thermostat is off switch to which mode?") { + input turnOnTherm + } + section("Set the thermostat to the following temps") { + input coolingTemp + input heatingTemp + } + section("For how long?") { + input turnOffDelay2 + } + section("In addtion to 'app touch' the following modes will also boost the thermostat") { + input modes1 + } + } + +} + +// Show "Setup" page +def Settings() { + + def sendPushMessage = [ + name: "sendPushMessage", + type: "enum", + title: "Send a push notification?", + metadata: [values:["Yes","No"]], + required: true, + defaultValue: "Yes" + ] + + def phoneNumber = [ + name: "phoneNumber", + type: "phone", + title: "Send SMS notifications to?", + required: false + ] + + def days = [ + name: "days", + type: "enum", + title: "Only on certain days of the week", + multiple: true, + required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + ] + + def modes = [ + name: "modes", + type: "mode", + title: "Only when mode is", + multiple: true, + required: false + ] + + def pageName = "Settings" + + def pageProperties = [ + name: "Settings", + title: "Settings", + nextPage: "pageSetup" + ] + + return dynamicPage(pageProperties) { + + + section( "Notifications" ) { + input sendPushMessage + input phoneNumber + } + section(title: "More options", hideable: true) { + href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true + input days + input modes + } + } + +} + +def installed(){ + init() +} + +def updated(){ + unsubscribe() + init() +} + +def init(){ + state.lastStatus = null + subscribe(app, appTouch) + runIn(60, "temperatureHandler") + subscribe(sensor, "temperature", temperatureHandler) + if(modes1){ + subscribe(location, modeBoostChange) + } + if(doors){ + subscribe(doors, "contact.open", temperatureHandler) + subscribe(doors, "contact.closed", doorCheck) + } +} + +def temperatureHandler(evt) { + if(modeOk && daysOk && timeOk) { + if(setLow > setHigh){ + def temp = setLow + setLow = setHigh + setHigh = temp + } + if (doorsOk) { + def currentTemp = sensor.latestValue("temperature") + if (currentTemp < setLow) { + if (state.lastStatus == "two" || state.lastStatus == "three" || state.lastStatus == null){ + //log.info "Setting thermostat mode to ${cold}" + def msg = "I changed your thermostat mode to ${cold} because temperature is below ${setLow}" + thermostat?."${cold}"() + sendMessage(msg) + } + state.lastStatus = "one" + } + if (currentTemp > setHigh) { + if (state.lastStatus == "one" || state.lastStatus == "three" || state.lastStatus == null){ + //log.info "Setting thermostat mode to ${hot}" + def msg = "I changed your thermostat mode to ${hot} because temperature is above ${setHigh}" + thermostat?."${hot}"() + sendMessage(msg) + } + state.lastStatus = "two" + } + if (currentTemp > setLow && currentTemp < setHigh) { + if (state.lastStatus == "two" || state.lastStatus == "one" || state.lastStatus == null){ + //log.info "Setting thermostat mode to ${neutral}" + def msg = "I changed your thermostat mode to ${neutral} because temperature is neutral" + thermostat?."${neutral}"() + sendMessage(msg) + } + state.lastStatus = "three" + } + } + else{ + def delay = (turnOffDelay != null && turnOffDelay != "") ? turnOffDelay * 60 : 60 + log.debug("Detected open doors. Checking door states again") + runIn(delay, "doorCheck") + } + } +} + +def appTouch(evt) { +if(thermostat1){ + state.lastStatus = "disabled" + def currentCoolSetpoint = thermostat1.latestValue("coolingSetpoint") as String + def currentHeatSetpoint = thermostat1.latestValue("heatingSetpoint") as String + def currentMode = thermostat1.latestValue("thermostatMode") as String + def mode = turnOnTherm + state.currentCoolSetpoint1 = currentCoolSetpoint + state.currentHeatSetpoint1 = currentHeatSetpoint + state.currentMode1 = currentMode + + thermostat1."${mode}"() + thermostat1.setCoolingSetpoint(coolingTemp) + thermostat1.setHeatingSetpoint(heatingTemp) + + thermoShutOffTrigger() + //log.debug("current coolingsetpoint is ${state.currentCoolSetpoint1}") + //log.debug("current heatingsetpoint is ${state.currentHeatSetpoint1}") + //log.debug("current mode is ${state.currentMode1}") +} +} + +def modeBoostChange(evt) { + if(thermostat1 && modes1.contains(location.mode)){ + state.lastStatus = "disabled" + def currentCoolSetpoint = thermostat1.latestValue("coolingSetpoint") as String + def currentHeatSetpoint = thermostat1.latestValue("heatingSetpoint") as String + def currentMode = thermostat1.latestValue("thermostatMode") as String + def mode = turnOnTherm + state.currentCoolSetpoint1 = currentCoolSetpoint + state.currentHeatSetpoint1 = currentHeatSetpoint + state.currentMode1 = currentMode + + thermostat1."${mode}"() + thermostat1.setCoolingSetpoint(coolingTemp) + thermostat1.setHeatingSetpoint(heatingTemp) + + log.debug("current coolingsetpoint is ${state.currentCoolSetpoint1}") + log.debug("current heatingsetpoint is ${state.currentHeatSetpoint1}") + log.debug("current mode is ${state.currentMode1}") + } + else{ + thermoShutOff() + } +} + +def thermoShutOffTrigger() { + //log.info("Starting timer to turn off thermostat") + def delay = (turnOffDelay2 != null && turnOffDelay2 != "") ? turnOffDelay2 * 60 : 60 + state.turnOffTime = now() + log.debug ("Turn off delay is ${delay}") + runIn(delay, "thermoShutOff") + } + +def thermoShutOff(){ + if(state.lastStatus == "disabled"){ + def coolSetpoint = state.currentCoolSetpoint1 + def heatSetpoint = state.currentHeatSetpoint1 + def mode = state.currentMode1 + def coolSetpoint1 = coolSetpoint.replaceAll("\\]", "").replaceAll("\\[", "") + def heatSetpoint1 = heatSetpoint.replaceAll("\\]", "").replaceAll("\\[", "") + def mode1 = mode.replaceAll("\\]", "").replaceAll("\\[", "") + + state.lastStatus = null + //log.info("Returning thermostat back to normal") + thermostat1.setCoolingSetpoint("${coolSetpoint1}") + thermostat1.setHeatingSetpoint("${heatSetpoint1}") + thermostat1."${mode1}"() + temperatureHandler() + } +} + +def doorCheck(evt){ + if (!doorsOk){ + log.debug("doors still open turning off ${thermostat}") + def msg = "I changed your thermostat mode to off because some doors are open" + + if (state.lastStatus != "off"){ + thermostat?.off() + sendMessage(msg) + } + state.lastStatus = "off" + } + + else{ + if (state.lastStatus == "off"){ + state.lastStatus = null + } + temperatureHandler() + } +} + +private sendMessage(msg){ + if (sendPushMessage == "Yes") { + sendPush(msg) + } + if (phoneNumber != null) { + sendSms(phoneNumber, msg) + } +} + +private getAllOk() { + modeOk && daysOk && timeOk && doorsOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDoorsOk() { + def result = !doors || !doors.latestValue("contact").contains("open") + log.trace "doorsOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + + else if (starting){ + result = currTime >= start + } + else if (ending){ + result = currTime <= stop + } + + log.trace "timeOk = $result" + result +} + +def getTimeLabel(starting, ending){ + + def timeLabel = "Tap to set" + + if(starting && ending){ + timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) + } + else if (starting) { + timeLabel = "Start at" + " " + hhmm(starting) + } + else if(ending){ + timeLabel = "End at" + hhmm(ending) + } + timeLabel +} + +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} +def greyedOut(){ + def result = "" + if (sensor) { + result = "complete" + } + result +} + +def greyedOutTherm(){ + def result = "" + if (thermostat) { + result = "complete" + } + result +} + +def greyedOutTherm1(){ + def result = "" + if (thermostat1) { + result = "complete" + } + result +} + +def greyedOutSettings(){ + def result = "" + if (starting || ending || days || modes || sendPushMessage) { + result = "complete" + } + result +} + +def greyedOutTime(starting, ending){ + def result = "" + if (starting || ending) { + result = "complete" + } + result +} + +private anyoneIsHome() { + def result = false + + if(people.findAll { it?.currentPresence == "present" }) { + result = true + } + + log.debug("anyoneIsHome: ${result}") + + return result +} + +page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { + section { + input "starting", "time", title: "Starting (both are required)", required: false + input "ending", "time", title: "Ending (both are required)", required: false + } + } \ No newline at end of file diff --git a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy new file mode 100644 index 00000000000..f71c126fe80 --- /dev/null +++ b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy @@ -0,0 +1,385 @@ +/** + * Vacation Lighting Director + * + * Version 2.4 - Added information paragraphs + * + * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy + * + * Copyright 2015 Tim Slagle + * + * 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. + * + */ + + +// Automatically generated. Make future change here. +definition( + name: "Vacation Lighting Director", + namespace: "tslagle13", + author: "Tim Slagle", + category: "Safety & Security", + description: "Randomly turn on/off lights to simulate the appearance of a occupied home while you are away.", + iconUrl: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png", + iconX2Url: "http://icons.iconarchive.com/icons/custom-icon-design/mono-general-2/512/settings-icon.png" +) + +preferences { + page name:"pageSetup" + page name:"Setup" + page name:"Settings" + +} + +// Show setup page +def pageSetup() { + + def pageProperties = [ + name: "pageSetup", + title: "Status", + nextPage: null, + install: true, + uninstall: true + ] + + return dynamicPage(pageProperties) { + section(""){ + paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " + + "Please use each othe the sections below to setup the different preferences to your liking. " + + "I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. " + } + section("Setup Menu") { + href "Setup", title: "Setup", description: "", state:greyedOut() + href "Settings", title: "Settings", description: "", state: greyedOutSettings() + } + section([title:"Options", mobileOnly:true]) { + label title:"Assign a name", required:false + } + } +} + +// Show "Setup" page +def Setup() { + + def newMode = [ + name: "newMode", + type: "mode", + title: "Which?", + multiple: true, + required: true + ] + def switches = [ + name: "switches", + type: "capability.switch", + title: "Switches", + multiple: true, + required: true + ] + + def frequency_minutes = [ + name: "frequency_minutes", + type: "number", + title: "Minutes?", + required: true + ] + + def number_of_active_lights = [ + name: "number_of_active_lights", + type: "number", + title: "Number of active lights", + required: true, + ] + + def people = [ + name: "people", + type: "capability.presenceSensor", + title: "If these people are home do not change light status", + required: true, + multiple: true + ] + + def pageName = "Setup" + + def pageProperties = [ + name: "Setup", + title: "Setup", + nextPage: "pageSetup" + ] + + return dynamicPage(pageProperties) { + + section(""){ + paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " + + paragraph "you are away. All of these settings are required in order for the simulator to run correctly." + } + section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") { + input newMode + } + section("Light switches to turn on/off") { + input switches + } + section("How often to cycle the lights") { + input frequency_minutes + } + section("Number of active lights at any given time") { + input number_of_active_lights + } + section("People") { + input people + } + } + +} + +// Show "Setup" page +def Settings() { + + def falseAlarmThreshold = [ + name: "falseAlarmThreshold", + type: "decimal", + title: "Default is 2 minutes", + required: false + ] + def days = [ + name: "days", + type: "enum", + title: "Only on certain days of the week", + multiple: true, + required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + ] + + def pageName = "Settings" + + def pageProperties = [ + name: "Settings", + title: "Settings", + nextPage: "pageSetup" + ] + + return dynamicPage(pageProperties) { + + section(""){ + paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " + + paragraph "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change." + } + section("Delay to start simulator") { + input falseAlarmThreshold + } + section("More options") { + href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true + input days + } + } +} + +page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } +} + +def installed() { +initialize() +} + +def updated() { + unsubscribe(); + unschedule(); + initialize() +} + +def initialize(){ + + if (newMode != null) { + subscribe(location, modeChangeHandler) + } +} + +def modeChangeHandler(evt) { + log.debug "Mode change to: ${evt.value}" + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60 + runIn(delay, scheduleCheck) +} + + +//Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off +def scheduleCheck(evt) { +if(allOk){ +log.debug("Running") + // turn off all the switches + switches.off() + + // grab a random switch + def random = new Random() + def inactive_switches = switches + for (int i = 0 ; i < number_of_active_lights ; i++) { + // if there are no inactive switches to turn on then let's break + if (inactive_switches.size() == 0){ + break + } + + // grab a random switch and turn it on + def random_int = random.nextInt(inactive_switches.size()) + inactive_switches[random_int].on() + + // then remove that switch from the pool off switches that can be turned on + inactive_switches.remove(random_int) + } + + // re-run again when the frequency demands it + runIn(frequency_minutes * 60, scheduleCheck) +} +//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period. +else if (modeOk) { + log.debug("mode OK. Running again") + runIn(frequency_minutes * 60, scheduleCheck) + switches.off() +} +//if none is ok turn off frequency check and turn off lights. +else if(people){ + //don't turn off lights if anyone is home + if(someoneIsHome()){ + log.debug("Stopping Check for Light") + } + else{ + log.debug("Stopping Check for Light and turning off all lights") + switches.off() + } +} +} + + +//below is used to check restrictions +private getAllOk() { + modeOk && daysOk && timeOk && homeIsEmpty +} + + +private getModeOk() { + def result = !newMode || newMode.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + + else if (starting){ + result = currTime >= start + } + else if (ending){ + result = currTime <= stop + } + + log.trace "timeOk = $result" + result +} + +private getHomeIsEmpty() { + def result = true + + if(people?.findAll { it?.currentPresence == "present" }) { + result = false + } + + log.debug("homeIsEmpty: ${result}") + + return result +} + +private getSomeoneIsHome() { + def result = false + + if(people?.findAll { it?.currentPresence == "present" }) { + result = true + } + + log.debug("anyoneIsHome: ${result}") + + return result +} + + +//gets the label for time restriction. Label phrasing changes depending on if there is both start and stop times or just one start/stop time. +def getTimeLabel(starting, ending){ + + def timeLabel = "Tap to set" + + if(starting && ending){ + timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) + } + else if (starting) { + timeLabel = "Start at" + " " + hhmm(starting) + } + else if(ending){ + timeLabel = "End at" + hhmm(ending) + } + timeLabel +} + +//fomrats time to readable format for time label +private hhmm(time, fmt = "h:mm a") +{ + def t = timeToday(time, location.timeZone) + def f = new java.text.SimpleDateFormat(fmt) + f.setTimeZone(location.timeZone ?: timeZone(time)) + f.format(t) +} + +//sets complete/not complete for the setup section on the main dynamic page +def greyedOut(){ + def result = "" + if (switches) { + result = "complete" + } + result +} + +//sets complete/not complete for the settings section on the main dynamic page +def greyedOutSettings(){ + def result = "" + if (starting || ending || days || falseAlarmThreshold) { + result = "complete" + } + result +} + +//sets complete/not complete for time restriction section in settings +def greyedOutTime(starting, ending){ + def result = "" + if (starting || ending) { + result = "complete" + } + result +} diff --git a/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy b/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy new file mode 100644 index 00000000000..299b494920f --- /dev/null +++ b/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy @@ -0,0 +1,86 @@ +/** + * Lock it at a specific time + * + * Copyright 2014 Erik Thayer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +definition( + name: "Lock it at a specific time", + namespace: "user8798", + author: "Erik Thayer", + description: "Make sure a door is locked at a specific time. Option to add door contact sensor to only lock if closed.", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" +) + + +preferences { + section("At this time every day") { + input "time", "time", title: "Time of Day" + } + section("Make sure this is locked") { + input "lock","capability.lock" + } + section("Make sure it's closed first..."){ + input "contact", "capability.contactSensor", title: "Which contact sensor?", required: false + } + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false + input "phone", "phone", title: "Send a text message?", required: false + } +} +def installed() { + schedule(time, "setTimeCallback") + +} + +def updated(settings) { + unschedule() + schedule(time, "setTimeCallback") +} + +def setTimeCallback() { + if (contact) { + doorOpenCheck() + } else { + lockMessage() + lock.lock() + } +} +def doorOpenCheck() { + def currentState = contact.contactState + if (currentState?.value == "open") { + def msg = "${contact.displayName} is open. Scheduled lock failed." + log.info msg + if (sendPushMessage) { + sendPush msg + } + if (phone) { + sendSms phone, msg + } + } else { + lockMessage() + lock.lock() + } +} + +def lockMessage() { + def msg = "Locking ${lock.displayName} due to scheduled lock." + log.info msg + if (sendPushMessage) { + sendPush msg + } + if (phone) { + sendSms phone, msg + } +} diff --git a/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy b/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy new file mode 100644 index 00000000000..3d5f9550059 --- /dev/null +++ b/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy @@ -0,0 +1,129 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Alfred Workflow + * + * Author: SmartThings + */ + +definition( + name: "Alfred Workflow", + namespace: "vlaminck", + author: "SmartThings", + description: "This SmartApp allows you to interact with the things in your physical graph through Alfred.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app@2x.png", + oauth: [displayName: "SmartThings Alfred Workflow", displayLink: ""] +) + +preferences { + section("Allow Alfred to Control These Things...") { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + } +} + +mappings { + path("/switches") { + action: [ + GET: "listSwitches", + PUT: "updateSwitches" + ] + } + path("/switches/:id") { + action: [ + GET: "showSwitch", + PUT: "updateSwitch" + ] + } + path("/locks") { + action: [ + GET: "listLocks", + PUT: "updateLocks" + ] + } + path("/locks/:id") { + action: [ + GET: "showLock", + PUT: "updateLock" + ] + } +} + +def installed() {} + +def updated() {} + +def listSwitches() { + switches.collect { device(it,"switch") } +} +void updateSwitches() { + updateAll(switches) +} +def showSwitch() { + show(switches, "switch") +} +void updateSwitch() { + update(switches) +} + +def listLocks() { + locks.collect { device(it, "lock") } +} +void updateLocks() { + updateAll(locks) +} +def showLock() { + show(locks, "lock") +} +void updateLock() { + update(locks) +} + +private void updateAll(devices) { + def command = request.JSON?.command + if (command) { + devices."$command"() + } +} + +private void update(devices) { + log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" + def command = request.JSON?.command + if (command) { + def device = devices.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + device."$command"() + } + } +} + +private show(devices, name) { + def device = devices.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } + else { + def s = device.currentState(name) + [id: device.id, label: device.displayName, name: device.displayName, state: s] + } +} + +private device(it, name) { + if (it) { + def s = it.currentState(name) + [id: it.id, label: it.displayName, name: it.displayName, state: s] + } +} diff --git a/smartapps/vlaminck/minecraft/smartblock-chat-sender.src/smartblock-chat-sender.groovy b/smartapps/vlaminck/minecraft/smartblock-chat-sender.src/smartblock-chat-sender.groovy new file mode 100644 index 00000000000..c4401fd6f90 --- /dev/null +++ b/smartapps/vlaminck/minecraft/smartblock-chat-sender.src/smartblock-chat-sender.groovy @@ -0,0 +1,87 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartClockChatSender + * + * Author: Steve Vlaminck + * + * Date: 2014-02-03 + */ + +definition( + name: "SmartBlock Chat Sender", + namespace: "vlaminck/Minecraft", + author: "SmartThings", + description: "Send chat messages into Minecraft via the SmartBlock mod", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + page(name: "chatPage", title: "Send Notifications Into Minecraft", install: true, uninstall: true) { + section { + input(name: "chatsEnabled", type: "bool", title: "Enable This Notification?", defaultValue: "true") + input(name: "modes", type: "mode", title: "Notify SmartBlock users when the mode changes to:", description: "(optional)", multiple: true, required: false) + input(name: "username", type: "string", title: "Only send to this username", required: false, description: "(optional)") + input(name: "customMessage", type: "string", title: "Custom message?", required: false, description: "(optional)") + } + section(hidden: true, hideable: true, title: "Other Options") { + label(title: "Label this Notification", required: false) + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(location, modeChangeHandler) +} + +def modeChangeHandler(evt) { + def newMode = evt.value + log.debug "evt: ${newMode}" + if (modes && modes.contains(newMode)) + { + def message = customMessage ?: "SmartThings mode has changed to: \"${newMode}\"" + chatMessageToMC(message) + } +} + +def chatMessageToMC(message) { + + def parent = app.getParent() + + def url = "${parent.getServerURL()}/chat?message=${message.encodeAsURL()}" + + if (username) + { + url += "&username=${username.encodeAsURL()}" + } + log.debug "POST to ${url}" + + httpPost(url, "foo=bar") { response -> + content = response.data + log.debug "response: ${content}" + } + +} diff --git a/smartapps/vlaminck/minecraft/smartblock-linker.src/smartblock-linker.groovy b/smartapps/vlaminck/minecraft/smartblock-linker.src/smartblock-linker.groovy new file mode 100644 index 00000000000..78540a9546a --- /dev/null +++ b/smartapps/vlaminck/minecraft/smartblock-linker.src/smartblock-linker.groovy @@ -0,0 +1,178 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartBlock Linker + * + * Author: Steve Vlaminck + * + * Date: 2013-12-26 + */ + +definition( + name: "SmartBlock Linker", + namespace: "vlaminck/Minecraft", + author: "SmartThings", + description: "A SmartApp that links SmartBlocks to switches", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + + page(name: "linkerPage") +} + +def linkerPage(params) { + + log.debug "linkerPage params: ${params}" + + dynamicPage(name: "linkerPage", title: "Link your SmartBlock to a physical device", install: true, uninstall: false) { + + section { + input( + name: "linkedSmartBlock", + type: "capability.switch", +// type: "device.SmartBlock", + title: "Linked SmartBlock", + required: true, + multiple: false + ) + input( + name: "switchUpdatesBlock", + type: "bool", + title: "Update this SmartBlock when the switch below changes state", + description: "", + defaultValue: "false" + ) + } + section { + input( + name: "linkedSwitch", + type: "capability.switch", + title: "Linked Switch", + required: true, + multiple: false + ) + input( + name: "blockUpdatesSwitch", + type: "bool", + title: "Update this switch when the SmartBlock above changes state", + description: "", + defaultValue: "true" + ) + } + + section { + label( + title: "Label this Link", + required: false + ) + mode( + title: "Only link these devices when in one of these modes", + description: "All modes" + ) + } + + section("When \"Update this SmartBlock...\" is on") { + paragraph "If you place a Redstone Lamp next to your SmartBlock, it will turn on/off when \"Linked Switch\" turns on/off" + } + + section("When \"Update this switch...\" is on") { + paragraph "If you place a lever on your Minecraft SmartBlock, it will control \"Linked Switch\"" + } + + section("Why turning both on can be bad") { + paragraph "Because there can be latency." + paragraph "Flipping the lever will send a signal from Minecraft to SmartThings. SmartThings will then send the signal back when the light has turned on." + paragraph "If you flip the lever again before that round trip is complete, you can get into an infinite loop of signals being sent back and forth." + paragraph "You've been warned ;)" + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + + if (blockUpdatesSwitch) + { + subscribe(linkedSmartBlock, "level", updateSwitchLevel) + subscribe(linkedSmartBlock, "switch", updateSwitchState) + } + + if (switchUpdatesBlock) + { + subscribe(linkedSwitch, "level", updateBlockLevel) + subscribe(linkedSwitch, "switch", updateBlockState) + } + +} + +def updateSwitchLevel(evt) { + int level = evt.value as int + log.debug "matching level: ${level}" + linkedSwitch.setLevel(level) +} + +def updateBlockLevel(evt) { + int level = evt.value as int + log.debug "matching level: ${level}" + linkedSmartBlock.setLevel(level) +} + +def updateSwitchState(evt) { + log.debug "setting linkedSwitch to ${evt.value}" + linkedSwitch."${evt.value}"() +} + +def updateBlockState(evt) { + log.debug "setting linkedSmartBlock to ${evt.value}" + linkedSmartBlock."${evt.value}"() +} + +def getBlockId() { + return linkedSmartBlock.id +} + +def getLinkerDescription() { + + def left = linkedSmartBlock ? "${linkedSmartBlock.label ?: linkedSmartBlock.name}" : "" + def right = linkedSwitch ? "${linkedSwitch.label ?: linkedSwitch.name}" : "" + + log.debug "left: ${left}, right: ${right}" + + def leftLink = switchUpdatesBlock ? "<" : "" + def rightLink = blockUpdatesSwitch ? ">" : "" + + log.debug "leftLink: ${leftLink}, rightLink: ${rightLink}" + + log.debug "switchUpdatesBlock: ${switchUpdatesBlock}" + log.debug "blockUpdatesSwitch: ${blockUpdatesSwitch}" + + if (leftLink == "" && rightLink == "") + { + return null + } + + "${left} ${leftLink}--${rightLink} ${right}" +} diff --git a/smartapps/vlaminck/minecraft/smartblock-manager.src/smartblock-manager.groovy b/smartapps/vlaminck/minecraft/smartblock-manager.src/smartblock-manager.groovy new file mode 100644 index 00000000000..c659d4a2157 --- /dev/null +++ b/smartapps/vlaminck/minecraft/smartblock-manager.src/smartblock-manager.groovy @@ -0,0 +1,289 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +import com.sun.corba.se.spi.activation._ServerImplBase + +/** + * SmartBlock Manager + * + * Author: Steve Vlaminck + * + * Date: 2013-12-24 + */ + +definition( + name: "SmartBlock Manager", + namespace: "vlaminck/Minecraft", + author: "SmartThings", + description: "A SmartApp for managing SmartBlocks", + iconUrl: "http://f.cl.ly/items/0p2c222z0p2K0y3y3w2M/SmartApp-icon.png", + iconX2Url: "http://f.cl.ly/items/0p2c222z0p2K0y3y3w2M/SmartApp-icon.png", + oauth: [displayName: "SmartBlock Manager", displayLink: ""] +) + +preferences { + + page(name: "listPage") + page(name: "serverPage") + page(name: "blockPage") + page(name: "linkerPage") + page(name: "notifierPage") + page(name: "chatPage") + + section("SmartBlock Manager") { + input name: "explanation1", title: "Every time you place a SmartBlock in Minecraft, a new SmartThings Device will be created. These Devices can be used in various SmartApps like \"Minecraft Notifier\", or \"Switch State Matcher\"", description: "", type: "paragraph", element: "paragraph", required: false + input name: "explanation2", title: "In order for SmartThings to send commands back to your Minecraft SmartBlocks, you will have to enter your Server Address via the SmartThings iOS or Android app", description: "", type: "paragraph", element: "paragraph", required: false + } +} + +def listPage() { + + log.debug "listPage" + + def linkerApps = findAllChildAppsByName("SmartBlock Linker") + def linkerState = linkerApps ? "complete" : "" + def linkerDescription = linkerApps.collect { it.label ?: it.name }.sort().join("\n") + + def notifierApps = findAllChildAppsByName("SmartBlock Notifier") + def notifierState = notifierApps ? "complete" : "" + def notifierDescription = notifierApps.collect { it.label ?: it.name }.sort().join("\n") + + def chatApps = findAllChildAppsByName("SmartBlock Chat Sender") + def chatState = chatApps ? "complete" : "" + def chatDescription = chatApps.collect { it.label ?: it.name }.sort().join("\n") + + return dynamicPage(name: "listPage", title: "Configure Your SmartBlocks", install: true, uninstall: true) { + section { + href( + name: "toLinkerPage", + title: "Link SmartBlocks To Switches", + description: linkerDescription, + page: "linkerPage", + state: linkerState + ) + href( + name: "toNotifierPage", + title: "Get Notified When a SmartBlock updates", + description: notifierDescription, + page: "notifierPage", + state: notifierState + ) + href( + name: "toChatPage", + title: "Send Notifications into Minecraft", + description: chatDescription, + page: "chatPage", + state: chatState + ) + } + + section { + input( + name: "serverIp", + title: "In order for SmartThings to send commands back to the SmartBlocks on your Minecraft server, you will have to enter your Server Address", + type: "text", + required: false + ) + } + } +} + +def serverPage() { + log.debug "serverPage" + dynamicPage(name: "serverPage", title: "Connect SmartThings To Your Minecraft Server") { + section { + input( + name: "serverIp", + title: "In order for SmartThings to send commands back to the SmartBlocks on your Minecraft server, you will have to enter your Server Address", + type: "text", + required: false + ) + } + } +} + + +def linkerPage() { + dynamicPage(name: "linkerPage", title: "Link SmartBlocks To Switches") { + section { + app( + title: "Link a SmartBlock to a switch", + name: "blockLinker-new", + namespace: "vlaminck/Minecraft", + appName: "SmartBlock Linker", + page: "linkerPage", + multiple: true, + params: ["blocks": getChildDevices()] + ) + } + } + +} + +def notifierPage() { + return dynamicPage(name: "notifierPage", title: "Get Notified When a SmartBlock is updated") { + section { + app( + title: "Get Notified", + name: "blockNotifier-new", + namespace: "vlaminck/Minecraft", + appName: "SmartBlock Notifier", + multiple: true + ) + } + } +} + +def chatPage() { + return dynamicPage(name: "chatPage", title: "Send Notifications into Minecraft") { + section { + app( + title: "Send Notifications", + name: "chatSender-new", + namespace: "vlaminck/Minecraft", + appName: "SmartBlock Chat Sender", + multiple: true + ) + } + } +} + +mappings { + path("/block") { // any need for GET? + action: + [ + POST : "createBlock", + PUT : "updateBlock", + DELETE: "deleteBlock" + ] + } + path("/ack") { + action: + [ + POST: "ack" + ] + } +} + +def createBlock() { + def data = request.JSON + def blockCoordinates = blockCoordinates(data) + def blockDNI = blockDNI(data) + def block = block(data) + + if (block) { + log.debug "Block ${block?.label} with id $blockDNI already exists" + } else { + block = addChildDevice("vlaminck/Minecraft", "Smart Block", blockDNI, null, [name: "SmartBlock", label: "SmartBlock $blockCoordinates"]) + } + + block?.setCoordinates(data.x, data.y, data.z) + block?.setDestroyed(false) + block?.setWorldSeed(data?.worldSeed) + block?.setDimensionName(data?.dimensionName) + block?.setPlacedBy(data?.placedBy) + + if (serverIp) { + block.setServerIp(serverIp) + } + + log.debug "created ${block?.label} with id $blockDNI" +} + +def ack() { + log.debug "ack params : $params" + log.debug "ack JSON : ${request.JSON}" + + sendDataToBlock(request?.JSON, false) +} + +def updateBlock() { + sendDataToBlock(request?.JSON, true) +} + +def sendDataToBlock(data, isStateChange) { + + def blockCoordinates = blockCoordinates(data) + def blockDNI = blockDNI(data) + def block = block(data) + log.debug "updating Block ${block?.label} with id $blockDNI" + + block?.neighborBlockChange(data) + + if (data.worldSeed) { + block.setWorldSeed(data.worldSeed) + } + + if (data.dimensionName) { + block.setDimensionName(data.dimensionName) + } + + if (data.placedBy) { + block.setPlacedBy(data.placedBy) + } + + block.setServerIp(serverIp) + +} + +def deleteBlock() { + def data = request.JSON + def blockDNI = blockDNI(data) + def block = block(data) + + block?.setDestroyed(true) + + + + log.debug "attempting to delete Block ${block?.label} with id $blockDNI" + deleteChildDevice(blockDNI) +} + +private blockCoordinates(data) { + return "(${data?.x},${data?.y},${data?.z})" +} + +private blockDNI(data) { + "${data.worldSeed}|${data.dimensionName}|${blockCoordinates(data)}".encodeAsMD5() +} + +private block(data) { + return getChildDevice(blockDNI(data)) +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + // update all children with serverIp. + if (serverIp) { + getChildDevices().each { block -> + block.setServerIp(serverIp) + } + } + +} + +public getServerURL() { + return "http://${serverIp}:3333" +} diff --git a/smartapps/vlaminck/minecraft/smartblock-notifier.src/smartblock-notifier.groovy b/smartapps/vlaminck/minecraft/smartblock-notifier.src/smartblock-notifier.groovy new file mode 100644 index 00000000000..3832df4fa2f --- /dev/null +++ b/smartapps/vlaminck/minecraft/smartblock-notifier.src/smartblock-notifier.groovy @@ -0,0 +1,989 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * SmartBlock Notifier + * + * Author: Steve Vlaminck + * + * Date: 2013-12-27 + */ + +definition( + name: "SmartBlock Notifier", + namespace: "vlaminck/Minecraft", + author: "SmartThings", + description: "A SmartApp that notifies you when things are happening around your SmartBlocks", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" +) + +preferences { + page(name: "firstPage") + page(name: "redstonePage") + page(name: "neighborBlockPage") + page(name: "messageBuilderPage") + page(name: "destroyedPage") +} + +def firstPage() { + + def defaultLabelValue = smartBlock ? (smartBlock.label ?: smartBlock.name) : null + + + def destroyedPageName = "destroyedPage" + def destroyedComplete = pageStateComplete(destroyedPageName) + def destroyedState = destroyedComplete ? "complete" : null + def destroyedDescription = destroyedComplete ? messageDescriptionForPage(destroyedPageName) : null + + def redstonePageName = "redstonePage" + def redstoneComplete = pageStateComplete(redstonePageName) + def redstoneState = redstoneComplete ? "complete" : null + def redstoneDescription = redstoneComplete ? messageDescriptionForPage(redstonePageName) : null + + def neighborPageName = "neighborBlockPage" + def neighborComplete = pageStateComplete(neighborPageName) + def neighborState = neighborComplete ? "complete" : null + def neighborDescription = neighborComplete ? messageDescriptionForPage(neighborPageNamePageName) : null + + dynamicPage(name: "firstPage", title: "Setup your notifications", install: true, uninstall: true) { + + section("Get notifications for this SmartBlock") { + input(name: "smartBlock", type: "capability.switch", title: "Which SmartBlock would you like to monitor?", multiple: false) + // TODO: type: "device.smartBlock", + } + + section("Why would you like to be notified?") { + href(name: "toDestroyedPage", page: destroyedPageName, title: "Because it was destroyed", description: destroyedDescription, state: destroyedState) + + href(name: "toRedstonePage", page: redstonePageName, title: "Because its redstone signal changed", description: redstoneDescription, state: redstoneState) + href(name: "toNeighborPage", page: neighborPageName, title: "Because a block next to it changed", description: neighborDescription, state: neighborState) + } + + section("Other Options") { + label(title: "Label this notification", description: app.name, required: false, defaultValue: defaultLabelValue) + mode(title: "Only send notifications when in one of these modes", description: "All modes") + } + } +} + +def destroyedPage() { + def pageName = "destroyedPage" + dynamicPage(name: pageName, title: "For when your block is destroyed") { + smartPhoneNotificationSection(pageName) + chatSection(pageName) + messageBuilderSection(pageName) + chatClosestPlayerSection(pageName) + } +} + +def redstonePage() { + def pageName = "redstonePage" + dynamicPage(name: pageName, title: "Get Notified For Redstone Changes") { + section("When Redstone Is") { + input(name: "redstoneGreaterThan", type: "enum", required: false, title: "Greater Than", options: (0..15).collect { + "${it}" + }) + input(name: "redstoneLessThan", type: "enum", required: false, title: "Less than", options: (0..15).collect { + "${it}" + }) + input(name: "redstoneEqualTo", type: "enum", required: false, title: "Equal to", options: (0..15).collect { + "${it}" + }) + } + smartPhoneNotificationSection(pageName) + chatSection(pageName) + messageBuilderSection(pageName) + } +} + +def neighborBlockPage() { + def pageName = "neighborBlockPage" + dynamicPage(name: pageName, title: "Get Notified When a neighbor block updates") { + section("Not all blocks send updates, but Chests definitely do") { + input(type: "enum", name: "neighborBlockParsed", title: "When any of these blocks are updated", required: false, multiple: true, options: allBlocksParsed()) + } + + smartPhoneNotificationSection(pageName) + chatSection(pageName) + messageBuilderSection(pageName) + chatClosestPlayerSection(pageName) + + section(title: "More Info", hideable: true, hidden: true) { + href(name: "allIds", title: "A full list of blocks and items can be found here", url: "http://minecraft.gamepedia.com/Ids", style: "external", description: null) + } + } +} + +def messageBuilderPage(params) { + + def pageName = params.pageName + def size = messageBuilderOptions().size() * 2 + + dynamicPage(name: "messageBuilderPage", title: "Build your message") { + section("These will be combined to form the final message.") { + (0..size).each { + input( + name: "${pageName}MessagePart${it}", + type: (it % 2) ? "enum" : "text", + defaultValue: messagePartDefaultValue(pageName, it), + options: (it % 2) ? messageBuilderOptions() : null, + title: null, description: null, required: false, multiple: false + ) + } + } + } +} + +def smartPhoneNotificationSection(pageName) { + section("SmartPhone notifications") { + input("recipients", "contact", title: "Send notifications to") { + input(name: "${pageName}WantsPush", title: "Push Notification", description: null, type: "bool", required: false, defaultValue: "false") + input(name: "${pageName}WantsSms", title: "Text Message", description: "phone number", type: "phone", required: false) + } + input(name: "${pageName}WantsHH", title: "Hello Home only", description: null, type: "bool", required: false) + } +} + +def chatSection(pageName) { + section("Minecraft Chat Message") { + input(name: "${pageName}ChatAllUsers", title: "Chat all users", type: "bool", required: false) + input(name: "${pageName}ChatUsername", title: "Or chat to a specific username", type: "text", required: false) + } +} + +def messageBuilderSection(pageName) { + section("What should your message say?") { + messageBuilderHref(pageName) + } +} + +def messageBuilderHref(pageName) { + def partsAreSet = messagePartsSet(pageName) + def messageState = partsAreSet ? "complete" : "" + def messageDescription = partsAreSet ? messageDescriptionForPage(pageName) : defaultMessageDescription(pageName) + + href( + name: "toBuilder", + page: "messageBuilderPage", + title: null, + description: messageDescription ?: "Construct your message", + state: messageState, + params: [pageName: pageName] + ) +} + +def chatClosestPlayerSection(pageName) { + section("Chat the closest player to the block. (usually the player that destroyed it)") { + messageBuilderHref("${pageName}ClosestPlayer") + } +} + +def pageStateComplete(pageName) { + + if (pageName == "redstonePage") { + if (redstoneGreaterThan) return true + if (redstoneLessThan) return true + if (redstoneEqualTo) return true + return false + } + + if (pageName == "neighborBlockPage") { + if (neighborBlockParsed) return true + return false + } + + if (app."${pageName}WantsPush") return true + if (app."${pageName}WantsSms") return true + if (app."${pageName}WantsHH") return true + if (app."${pageName}ChatAllUsers") return true + if (app."${pageName}ChatUsername") return true + if (app."${pageName}ClosestPlayer") return true + + return false +} + +/* +* INITIALIZE +*/ + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + log.debug "initializing" + subscribe(smartBlock, "redstoneSignalStrength", redstoneSignalStrengthHandler) + subscribe(smartBlock, "smartBlockNeighborChanged", smartBlockNeighborChangedHandler, [filterEvents: false]) + subscribe(smartBlock, "smartBlockNeighborChanged", smartBlockNeighborChangedHandler, [filterEvents: false]) + subscribe(smartBlock, "blockDestroyed.true", smartBlockDestroyedHandler, [filterEvents: false]) +} + +/* +* EVENT HANDLERS +*/ + +def smartBlockDestroyedHandler(evt) { + log.debug "smartBlockDestroyedHandler evt.value: ${evt.value}" + + def pageName = "destroyedPage" + def message = message(pageName) + notifyUser(pageName, message) +} + +def smartBlockNeighborChangedHandler(evt) { + log.debug "smartBlockNeighborChangedHandler evt.value: ${evt.value}" + log.debug "neighborBlockParsed: ${neighborBlockParsed}" + + if (neighborBlockParsed?.contains(evt.value)) { + notifyUserOfNeighborChange(evt.value) + } +} + +def redstoneSignalStrengthHandler(evt) { + log.debug "redstoneSignalStrengthHandler: ${evt.value}" + + int newValue = evt.value as int + int lastValue = smartBlock.latestState("redstoneSignalStrength").value as int + + if (redstoneGreaterThan) { + int gt = redstoneGreaterThan as int +// log.debug "$newValue > $gt" + if (newValue > gt) { + log.debug "greater than ${gt}. send notification" + notifyUserOfRedstoneChange(newValue) + } + } + + if (redstoneLessThan) { + int lt = redstoneLessThan as int +// log.debug "$newValue < $lt" + if (newValue < lt) { + log.debug "less than ${lt}. send notification" + notifyUserOfRedstoneChange(newValue) + } + } + + if (redstoneEqualTo) { + int et = redstoneEqualTo as int +// log.debug "$newValue == $et" + if (newValue == et) { + log.debug "equal to ${et}. send notification" + notifyUserOfRedstoneChange(newValue) + } + } + +} + +/* +* NOTIFICATIONS +*/ + +def notifyUserOfRedstoneChange(value) { + def msg = message("redstonePage") + log.debug "message: ${msg}" + def notificationMessage = msg ?: "${smartBlock} redstone signal is ${value}" + notifyUser(notificationMessage) +} + +def notifyUserOfNeighborChange(value) { + def msg = message("neighborPage") + log.debug "message: ${msg}" + def notificationMessage = msg ?: "${smartBlock} was updated by ${value}" + notifyUser(notificationMessage) +} + +def notifyUser(pageName, messageToSend) { + log.debug "notifyUser pageName: ${pageName}" + + def closestPlayerMessage = message("${pageName}ClosestPlayer") + log.debug "closestPlayerMessage = ${closestPlayerMessage}" + def latestClosePlayer = getLatestClosePlayer() + log.debug "latestClosePlayer = ${latestClosePlayer}" + if (closestPlayerMessage && latestClosePlayer != "unknown") { + log.debug "chatting closestPlayer" + chatMessageToMC(closestPlayerMessage, latestClosePlayer) + } + + + def wantsHH = app."${pageName}WantsHH" + log.debug "wantsHH = ${wantsHH}" + if (wantsHH) { + + log.debug "sending HH" + sendNotificationEvent(messageToSend) + + } else { + if (location.contactBookEnabled) { + sendNotificationToContacts(messageToSend, recipients) + } + else { + + def wantsPush = app."${pageName}WantsPush" + log.debug "wantsPush = ${wantsPush}" + if (wantsPush && wantsPush != "false") { + log.debug "sending push" + sendPush(messageToSend) + } + + def wantsSms = app."${pageName}WantsSms" + log.debug "wantsSms = ${wantsSms}" + if (wantsSms) { + log.debug "sending sms to: ${wantsSms}" + sendSms(wantsSms, messageToSend) + } + } + } + + def username = app."${pageName}ChatUsername" + def allUsers = app."${pageName}ChatAllUsers" + + log.debug "username = ${username}" + log.debug "allUsers = ${allUsers}" + + if (username && username != "") { + log.debug "chatting username: ${username}" + chatMessageToMC(messageToSend, username) + } else if (allUsers) { + log.debug "chatting all users" + chatMessageToMC(messageToSend, null) + } + +} + +def chatMessageToMC(message, username) { + log.debug "chatMessageToMC" + + def url = "${app.getParent().getServerURL()}/chat?message=${message.encodeAsURL()}" + if (username) { + url = "${url}&username=${username.encodeAsURL()}" + } + + log.debug "POST to ${url}" + + httpPost(url, "foo=bar") {} +} + +def messageDescriptionPartsForPage(pageName) { + def size = messageBuilderOptions().size() * 2 + (0..size).collect { app."${pageName}MessagePart${it}" } +} + +def messagePartsSet(pageName) { // are any set? + messageDescriptionPartsForPage(pageName).collect { !it }.unique().contains(false) +} + +def defaultMessageDescription(pageName) { + def description = "" + + if (pageName == "destroyedPage" || pageName == "redstonePage" || pageName == "neighborBlockPage") { + def second = messageBuilderOptions()[messagePartDefaultValue(pageName, 1)] + if (second) description = "\${${second}}" + + def third = messagePartDefaultValue(pageName, 2) + if (third) description = "${description} ${third}" + + def fourth = messageBuilderOptions()[messagePartDefaultValue(pageName, 3)] + if (fourth) description = "${description} \${${fourth}}" + } + + return description +} + +def messageDescriptionForPage(pageName) { + + def parts = messageDescriptionPartsForPage(pageName) + def messageParts = [] + parts.eachWithIndex { part, idx -> + if (part != null && part != "null") { + if (idx % 2) { + messageParts << "\${${messageBuilderOptions()[part]}}" + } else { + messageParts << part + } + } + } + + if (messageParts) { + return messageParts.join(" ").trim() + } else { + return defaultMessageDescription() + } +} + +def messagePartDefaultValue(pageName, part) { + if (pageName == "destroyedPage") { + if (part == 1) return "name" + if (part == 2) return "was destroyed by" + if (part == 3) return "closestPlayer" + } + + if (pageName == "neighborBlockPage") { + if (part == 1) return "name" + if (part == 2) return "has a redstone signal of" + if (part == 3) return "redstoneSignalStrength" + } + + if (pageName == "redstonePage") { + if (part == 1) return "name" + if (part == 2) return "was updated by" + if (part == 3) return "closestPlayer" + } + + return null +} + +def message(pageName) { + log.debug "building message" + def messageParts = [] + + messageDescriptionPartsForPage(pageName).eachWithIndex { part, idx -> + if (idx % 2) { +// def option = messageBuilderOptions()[part] + def optionPart = getMessagePartFromOption(part) + if (optionPart) messageParts << optionPart + } else { + if (part) messageParts << part + } + } + + def message = messageParts.join(" ").trim() + log.debug "message: ${message}" + return message +} + +def messageBuilderOptions() { + return [ + "name": "SmartBlock name", + "neighborBlockName": "Neighbor block name", + "blockDestroyed": "Destroyed State ('destroyed' / 'OK')", + "redstoneSignalStrength": "Redstone signal strength", + "worldSeed": "World seed", + "dimensionName": "Dimension name (World, Nether, End)", + "coordinates": "Block coordinates", + "closestPlayer": "Username of Closest player (within the past minute)", + "placedBy": "Username of who placed the block" + ] +} + +def getMessagePartFromOption(optionKey) { + log.debug "optionKey: ${optionKey}" + if (optionKey == "name") return smartBlock.label ?: smartBlock.name + if (optionKey == "closestPlayer") return getLatestClosePlayer() + if (optionKey == "blockDestroyed") return smartBlock.latestValue("blockDestroyed") ? "OK" : "destroyed" + return smartBlock.latestValue(optionKey) +} + +def getLatestClosePlayer() { + def now = new Date() + def minusOne = new Date(minutes: now.minutes - 1) + def latestStates = smartBlock.statesSince("closestPlayer", minusOne) + if (latestStates.size) { + return latestStates[0].value + } + return "unknown" +} + +/* +* BLOCKS +*/ + +def settingsAsIds() { + log.debug "settingsAsIds" + log.debug "neighborBlockParsed: $neighborBlockParsed" + + def subscribedIds = [] + + neighborBlockParsed.each { + subscribedIds << convertBlockSettingToBlockId(it) + } + + return subscribedIds +} + +def convertBlockSettingToBlockId(setting) { + def id = setting.substring(0, setting.indexOf(" ")) + def name = allBlocks()[id] + log.debug "id: $id, name:${name}" + return id +} + +def allBlocksParsed() { + allBlocks().collect { k, v -> "${k} ${v}" } +} + +def allBlocks() { + [ + "0": "Air", + "1": "Stone", + "2": "Grass", + "3": "Dirt", + "4": "Cobblestone", + "5": "Oak Wood Plank", + "5:1": "Spruce Wood Plank", + "5:2": "Birch Wood Plank", + "5:3": "Jungle Wood Plank", + "6": "Oak Sapling", + "6:1": "Spruce Sapling", + "6:2": "Birch Sapling", + "6:3": "Jungle Sapling", + "7": "Bedrock", + "8": "Water", + "9": "Stationary Water", + "10": "Lava", + "11": "Stationary Lava", + "12": "Sand", + "13": "Gravel", + "14": "Gold Ore", + "15": "Iron Ore", + "16": "Coal Ore", + "17": "Oak Wood", + "17:1": "Spruce Wood", + "17:2": "Birch Wood", + "17:3": "Jungle Wood", + "18": "Oak Leaves", + "18:1": "Spruce Leaves", + "18:2": "Birch Leaves", + "18:3": "Jungle Leaves", + "19": "Sponge", + "20": "Glass", + "21": "Lapis Lazuli Ore", + "22": "Lapis Lazuli Block", + "23": "Dispenser", + "24": "Sandstone", + "24:1": "Chiseled Sandstone", + "24:2": "Smooth Sandstone", + "25": "Note Block", + "26": "Bed Block", + "27": "Powered Rail", + "28": "Detector Rail", + "29": "Sticky Piston", + "30": "Web", + "31": "Dead Shrub", + "31:1": "Grass", + "31:2": "Fern", + "32": "Dead Shrub", + "33": "Piston", + "34": "Piston Head", + "35": "White Wool", + "35:1": "Orange Wool", + "35:2": "Magenta Wool", + "35:3": "Light Blue Wool", + "35:4": "Yellow Wool", + "35:5": "Lime Wool", + "35:6": "Pink Wool", + "35:7": "Gray Wool", + "35:8": "Light Gray Wool", + "35:9": "Cyan Wool", + "35:10": "Purple Wool", + "35:11": "Blue Wool", + "35:12": "Brown Wool", + "35:13": "Green Wool", + "35:14": "Red Wool", + "35:15": "Black Wool", + "37": "Dandelion", + "38": "Rose", + "39": "Brown Mushroom", + "40": "Red Mushroom", + "41": "Gold Block", + "42": "Iron Block", + "43": "Double Stone Slab", + "43:1": "Double Sandstone Slab", + "43:2": "Double Wooden Slab", + "43:3": "Double Cobblestone Slab", + "43:4": "Double Brick Slab", + "43:5": "Double Stone Brick Slab", + "43:6": "Double Nether Brick Slab", + "43:7": "Double Quartz Slab", + "44": "Stone Slab", + "44:1": "Sandstone Slab", + "44:2": "Wooden Slab", + "44:3": "Cobblestone Slab", + "44:4": "Brick Slab", + "44:5": "Stone Brick Slab", + "44:6": "Nether Brick Slab", + "44:7": "Quartz Slab", + "45": "Brick", + "46": "TNT", + "47": "Bookshelf", + "48": "Mossy Cobblestone", + "49": "Obsidian", + "50": "Torch", + "51": "Fire", + "52": "Monster Spawner", + "53": "Oak Wood Stairs", + "54": "Chest", + "55": "Redstone Wire", + "56": "Diamond Ore", + "57": "Diamond Block", + "58": "Workbench", + "59": "Wheat Crops", + "60": "Soil", + "61": "Furnace", + "62": "Burning Furnace", + "63": "Sign Post", + "64": "Wooden Door Block", + "65": "Ladder", + "66": "Rails", + "67": "Cobblestone Stairs", + "68": "Wall Sign", + "69": "Lever", + "70": "Stone Pressure Plate", + "71": "Iron Door Block", + "72": "Wooden Pressure Plate", + "73": "Redstone Ore", + "74": "Glowing Redstone Ore", + "75": "Redstone Torch(off)", + "76": "Redstone Torch(on)", + "77": "Stone Button", + "78": "Snow", + "79": "Ice", + "80": "Snow Block", + "81": "Cactus", + "82": "Clay", + "83": "Sugar Cane", + "84": "Jukebox", + "85": "Fence", + "86": "Pumpkin", + "87": "Netherrack", + "88": "Soul Sand", + "89": "Glowstone", + "90": "Portal", + "91": "Jack - O - Lantern", + "92": "Cake Block", + "93": "Redstone Repeater Block(off)", + "94": "Redstone Repeater Block(on)", + "95": "Locked Chest", + "96": "Trapdoor", + "97": "Stone(Silverfish)", + "97:1": "Cobblestone(Silverfish)", + "97:2": "Stone Brick(Silverfish)", + "98": "Stone Brick", + "98:1": "Mossy Stone Brick", + "98:2": "Cracked Stone Brick", + "98:3": "Chiseled Stone Brick", + "99": "Red Mushroom Cap", + "100": "Brown Mushroom Cap", + "101": "Iron Bars", + "102": "Glass Pane", + "103": "Melon Block", + "104": "Pumpkin Stem", + "105": "Melon Stem", + "106": "Vines", + "107": "Fence Gate", + "108": "Brick Stairs", + "109": "Stone Brick Stairs", + "110": "Mycelium", + "111": "Lily Pad", + "112": "Nether Brick", + "113": "Nether Brick Fence", + "114": "Nether Brick Stairs", + "115": "Nether Wart", + "116": "Enchantment Table", + "117": "Brewing Stand", + "118": "Cauldron", + "119": "End Portal", + "120": "End Portal Frame", + "121": "End Stone", + "122": "Dragon Egg", + "123": "Redstone Lamp(inactive)", + "124": "Redstone Lamp(active)", + "125": "Double Oak Wood Slab", + "125:1": "Double Spruce Wood Slab", + "125:2": "Double Birch Wood Slab", + "125:3": "Double Jungle Wood Slab", + "126": "Oak Wood Slab", + "126:1": "Spruce Wood Slab", + "126:2": "Birch Wood Slab", + "126:3": "Jungle Wood Slab", + "127": "Cocoa Plant", + "128": "Sandstone Stairs", + "129": "Emerald Ore", + "130": "Ender Chest", + "131": "Tripwire Hook", + "132": "Tripwire", + "133": "Emerald Block", + "134": "Spruce Wood Stairs", + "135": "Birch Wood Stairs", + "136": "Jungle Wood Stairs", + "137": "Command Block", + "138": "Beacon Block", + "139": "Cobblestone Wall", + "139:1": "Mossy Cobblestone Wall", + "140": "Flower Pot", + "141": "Carrots", + "142": "Potatoes", + "143": "Wooden Button", + "144": "Mob Head", + "145": "Anvil", + "146": "Trapped Chest", + "147": "Weighted Pressure Plate(light)", + "148": "Weighted Pressure Plate(heavy)", + "149": "Redstone Comparator(inactive)", + "150": "Redstone Comparator(active)", + "151": "Daylight Sensor", + "152": "Redstone Block", + "153": "Nether Quartz Ore", + "154": "Hopper", + "155": "Quartz Block", + "155:1": "Chiseled Quartz Block", + "155:2": "Pillar Quartz Block", + "156": "Quartz Stairs", + "157": "Activator Rail", + "158": "Dropper", + "159": "White Stained Clay", + "159:1": "Orange Stained Clay", + "159:2": "Magenta Stained Clay", + "159:3": "Light Blue Stained Clay", + "159:4": "Yellow Stained Clay", + "159:5": "Lime Stained Clay", + "159:6": "Pink Stained Clay", + "159:7": "Gray Stained Clay", + "159:8": "Light Gray Stained Clay", + "159:9": "Cyan Stained Clay", + "159:10": "Purple Stained Clay", + "159:11": "Blue Stained Clay", + "159:12": "Brown Stained Clay", + "159:13": "Green Stained Clay", + "159:14": "Red Stained Clay", + "159:15": "Black Stained Clay", + "170": "Hay Bale", + "171": "White Carpet", + "171:1": "Orange Carpet", + "171:2": "Magenta Carpet", + "171:3": "Light Blue Carpet", + "171:4": "Yellow Carpet", + "171:5": "Lime Carpet", + "171:6": "Pink Carpet", + "171:7": "Gray Carpet", + "171:8": "Light Gray Carpet", + "171:9": "Cyan Carpet", + "171:10": "Purple Carpet", + "171:11": "Blue Carpet", + "171:12": "Brown Carpet", + "171:13": "Green Carpet", + "171:14": "Red Carpet", + "171:15": "Black Carpet", + "172": "Hardened Clay", + "173": "Block of Coal", + "256": "Iron Shovel", + "257": "Iron Pickaxe", + "258": "Iron Axe", + "259": "Flint and Steel", + "260": "Apple", + "261": "Bow", + "262": "Arrow", + "263": "Coal", + "263:1": "Charcoal", + "264": "Diamond", + "265": "Iron Ingot", + "266": "Gold Ingot", + "267": "Iron Sword", + "268": "Wooden Sword", + "269": "Wooden Shovel", + "270": "Wooden Pickaxe", + "271": "Wooden Axe", + "272": "Stone Sword", + "273": "Stone Shovel", + "274": "Stone Pickaxe", + "275": "Stone Axe", + "276": "Diamond Sword", + "277": "Diamond Shovel", + "278": "Diamond Pickaxe", + "279": "Diamond Axe", + "280": "Stick", + "281": "Bowl", + "282": "Mushroom Soup", + "283": "Gold Sword", + "284": "Gold Shovel", + "285": "Gold Pickaxe", + "286": "Gold Axe", + "287": "String", + "288": "Feather", + "289": "Sulphur", + "290": "Wooden Hoe", + "291": "Stone Hoe", + "292": "Iron Hoe", + "293": "Diamond Hoe", + "294": "Gold Hoe", + "295": "Wheat Seeds", + "296": "Wheat", + "297": "Bread", + "298": "Leather Helmet", + "299": "Leather Chestplate", + "300": "Leather Leggings", + "301": "Leather Boots", + "302": "Chainmail Helmet", + "303": "Chainmail Chestplate", + "304": "Chainmail Leggings", + "305": "Chainmail Boots", + "306": "Iron Helmet", + "307": "Iron Chestplate", + "308": "Iron Leggings", + "309": "Iron Boots", + "310": "Diamond Helmet", + "311": "Diamond Chestplate", + "312": "Diamond Leggings", + "313": "Diamond Boots", + "314": "Gold Helmet", + "315": "Gold Chestplate", + "316": "Gold Leggings", + "317": "Gold Boots", + "318": "Flint", + "319": "Raw Porkchop", + "320": "Cooked Porkchop", + "321": "Painting", + "322": "Golden Apple", + "322:1": "Enchanted Golden Apple", + "323": "Sign", + "324": "Wooden Door", + "325": "Bucket", + "326": "Water Bucket", + "327": "Lava Bucket", + "328": "Minecart", + "329": "Saddle", + "330": "Iron Door", + "331": "Redstone", + "332": "Snowball", + "333": "Boat", + "334": "Leather", + "335": "Milk Bucket", + "336": "Clay Brick", + "337": "Clay Balls", + "338": "Sugarcane", + "339": "Paper", + "340": "Book", + "341": "Slimeball", + "342": "Storage Minecart", + "343": "Powered Minecart", + "344": "Egg", + "345": "Compass", + "346": "Fishing Rod", + "347": "Clock", + "348": "Glowstone Dust", + "349": "Raw Fish", + "350": "Cooked Fish", + "351": "Ink Sack", + "351:1": "Rose Red", + "351:2": "Cactus Green", + "351:3": "Coco Beans", + "351:4": "Lapis Lazuli", + "351:5": "Purple Dye", + "351:6": "Cyan Dye", + "351:7": "Light Gray Dye", + "351:8": "Gray Dye", + "351:9": "Pink Dye", + "351:10": "Lime Dye", + "351:11": "Dandelion Yellow", + "351:12": "Light Blue Dye", + "351:13": "Magenta Dye", + "351:14": "Orange Dye", + "351:15": "Bone Meal", + "352": "Bone", + "353": "Sugar", + "354": "Cake", + "355": "Bed", + "356": "Redstone Repeater", + "357": "Cookie", + "358": "Map", + "359": "Shears", + "360": "Melon", + "361": "Pumpkin Seeds", + "362": "Melon Seeds", + "363": "Raw Beef", + "364": "Steak", + "365": "Raw Chicken", + "366": "Cooked Chicken", + "367": "Rotten Flesh", + "368": "Ender Pearl", + "369": "Blaze Rod", + "370": "Ghast Tear", + "371": "Gold Nugget", + "372": "Nether Wart Seeds", + "373": "Potion", + "374": "Glass Bottle", + "375": "Spider Eye", + "376": "Fermented Spider Eye", + "377": "Blaze Powder", + "378": "Magma Cream", + "379": "Brewing Stand", + "380": "Cauldron", + "381": "Eye of Ender", + "382": "Glistering Melon", + "383:50": "Spawn Creeper", + "383:51": "Spawn Skeleton", + "383:52": "Spawn Spider", + "383:54": "Spawn Zombie", + "383:55": "Spawn Slime", + "383:56": "Spawn Ghast", + "383:57": "Spawn Pigman", + "383:58": "Spawn Enderman", + "383:59": "Spawn Cave Spider", + "383:60": "Spawn Silverfish ", + "383:61": "Spawn Blaze", + "383:62": "Spawn Magma Cube ", + "383:65": "Spawn Bat", + "383:66": "Spawn Witch", + "383:90": "Spawn Pig", + "383:91": "Spawn Sheep", + "383:92": "Spawn Cow", + "383:93": "Spawn Chicken", + "383:94": "Spawn Squid", + "383:95": "Spawn Wolf", + "383:96": "Spawn Mooshroom", + "383:98": "Spawn Ocelot", + "383:100": "Spawn Horse", + "383:120": "Spawn Villager", + "384": "Bottle o' Enchanting", + "385": "Fire Charge", + "386": "Book and Quill", + "387": "Written Book", + "388": "Emerald", + "389": "Item Frame", + "390": "Flower Pot", + "391": "Carrots", + "392": "Potato", + "393": "Baked Potato", + "394": "Poisonous Potato", + "395": "Map", + "396": "Golden Carrot", + "397": "Mob Head (Skeleton)", + "397:1": "Mob Head (Wither Skeleton)", + "397:2": "Mob Head (Zombie)", + "397:3": "Mob Head (Human)", + "397:4": "Mob Head (Creeper)", + "398": "Carrot on a Stick", + "399": "Nether Star", + "400": "Pumpkin Pie", + "401": "Firework Rocket", + "402": "Firework Star", + "403": "Enchanted Book", + "404": "Redstone Comparator", + "405": "Nether Brick", + "406": "Nether Quartz", + "407": "Minecart with TNT", + "408": "Minecart with Hopper", + "417": "Iron Horse Armor", + "418": "Gold Horse Armor", + "419": "Diamond Horse Armor", + "420": "Lead", + "421": "Name Tag" + ] +} diff --git a/smartapps/wackford/quirky-connect.src/quirky-connect.groovy b/smartapps/wackford/quirky-connect.src/quirky-connect.groovy new file mode 100644 index 00000000000..325f57d6438 --- /dev/null +++ b/smartapps/wackford/quirky-connect.src/quirky-connect.groovy @@ -0,0 +1,1117 @@ +/** + * Quirky (Connect) + * + * Author: todd@wackford.net + * Date: 2014-02-15 + * + * Update: 2014-02-22 + * Added eggtray + * Added device specific methods called from poll (versus in poll) + * + * Update2:2014-02-22 + * Added nimbus + * + * Update3:2014-02-26 + * Improved eggtray integration + * Added notifications to hello home + * Introduced Quirky Eggtray specific icons (Thanks to Dane) + * Added an Egg Report that outputs to hello home. + * Switched to Dan Lieberman's client and secret + * Still not browser flow (next update?) + * + * Update4:2014-03-08 + * Added Browser Flow OAuth + * + * + * Update5:2014-03-14 + * Added dynamic icon/tile updating to the nimbus. Changes the device icon from app. + * + * Update6:2014-03-31 + * Stubbed out creation and choice of nimbus, eggtray and porkfolio per request. + * + * Update7:2014-04-01 + * Renamed to 'Quirky (Connect)' and updated device names + * + * Update8:2014-04-08 (dlieberman) + * Stubbed out Spotter + * + * Update9:2014-04-08 (twackford) + * resubscribe to events on each poll + */ + +import java.text.DecimalFormat + +// Wink API +private apiUrl() { "https://winkapi.quirky.com/" } +private getVendorName() { "Quirky Wink" } +private getVendorAuthPath() { "https://winkapi.quirky.com/oauth2/authorize?" } +private getVendorTokenPath(){ "https://winkapi.quirky.com/oauth2/token?" } +private getVendorIcon() { "https://s3.amazonaws.com/smartthings-device-icons/custom/quirky/quirky-device@2x.png" } +private getClientId() { appSettings.clientId } +private getClientSecret() { appSettings.clientSecret } +private getServerUrl() { appSettings.serverUrl } + +definition( + name: "Quirky (Connect)", + namespace: "wackford", + author: "SmartThings", + description: "Connect your Quirky to SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png" +) { + appSetting "clientId" + appSetting "clientSecret" + appSetting "serverUrl" +} + +preferences { + page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false) + page(name: "listDevices", title: "Quirky Devices", content: "listDevices", install: false) +} + +mappings { + path("/receivedToken") { action:[ POST: "receivedToken", GET: "receivedToken"] } + path("/receiveToken") { action:[ POST: "receiveToken", GET: "receiveToken"] } + path("/powerstripCallback") { action:[ POST: "powerstripEventHandler", GET: "subscriberIdentifyVerification"]} + path("/sensor_podCallback") { action:[ POST: "sensor_podEventHandler", GET: "subscriberIdentifyVerification"]} + path("/piggy_bankCallback") { action:[ POST: "piggy_bankEventHandler", GET: "subscriberIdentifyVerification"]} + path("/eggtrayCallback") { action:[ POST: "eggtrayEventHandler", GET: "subscriberIdentifyVerification"]} + path("/cloud_clockCallback"){ action:[ POST: "cloud_clockEventHandler", GET: "subscriberIdentifyVerification"]} +} + +def authPage() { + log.debug "In authPage" + if(canInstallLabs()) { + def description = null + + if (state.vendorAccessToken == null) { + log.debug "About to create access token." + + createAccessToken() + description = "Tap to enter Credentials." + + def redirectUrl = oauthInitUrl() + + + return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) { + section { href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description } + } + } else { + description = "Tap 'Next' to proceed" + + return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) { + section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } + } + } + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + + return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section { + paragraph "$upgradeNeeded" + } + } + + } +} + +def oauthInitUrl() { + log.debug "In oauthInitUrl" + + /* OAuth Step 1: Request access code with our client ID */ + + state.oauthInitState = UUID.randomUUID().toString() + + def oauthParams = [ response_type: "code", + client_id: getClientId(), + state: state.oauthInitState, + redirect_uri: buildRedirectUrl("receiveToken") ] + + return getVendorAuthPath() + toQueryString(oauthParams) +} + +def buildRedirectUrl(endPoint) { + log.debug "In buildRedirectUrl" + + return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}" +} + +def receiveToken() { + log.debug "In receiveToken" + + def oauthParams = [ client_secret: getClientSecret(), + grant_type: "authorization_code", + code: params.code ] + + def tokenUrl = getVendorTokenPath() + toQueryString(oauthParams) + def params = [ + uri: tokenUrl, + ] + + /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ + httpPost(params) { response -> + + def data = response.data.data + + state.vendorRefreshToken = data.refresh_token //these may need to be adjusted depending on depth of returned data + state.vendorAccessToken = data.access_token + } + + if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install + return + } + + /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */ + + def html = """ + + + + + ${getVendorName()} Connection + + + +
+ Vendor icon + connected device icon + SmartThings logo +

We have located your """ + getVendorName() + """ account.

+

Tap 'Done' to process your credentials.

+
+ + + """ + render contentType: 'text/html', data: html +} + +def receivedToken() { + log.debug "In receivedToken" + + def html = """ + + + + + Withings Connection + + + +
+ Vendor icon + connected device icon + SmartThings logo +

Tap 'Done' to continue to Devices.

+
+ + + """ + render contentType: 'text/html', data: html +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + + +def subscriberIdentifyVerification() +{ + log.debug "In subscriberIdentifyVerification" + + def challengeToken = params.hub.challenge + + render contentType: 'text/plain', data: challengeToken +} + +def initialize() +{ + log.debug "Initialized with settings: ${settings}" + + //createAccessToken() + + //state.oauthInitState = UUID.randomUUID().toString() + + settings.devices.each { + def deviceId = it + + state.deviceDataArr.each { + if ( it.id == deviceId ) { + switch(it.type) { + + case "powerstrip": + log.debug "we have a Pivot Power Genius" + createPowerstripChildren(it.data) //has sub-devices, so we call out to create kids + createWinkSubscription( it.subsPath, it.subsSuff ) + break + + case "sensor_pod": + log.debug "we have a Spotter" + addChildDevice("wackford", "Quirky Wink Spotter", deviceId, null, [name: it.name, label: it.label, completedSetup: true]) + createWinkSubscription( it.subsPath, it.subsSuff ) + break + + case "piggy_bank": + log.debug "we have a Piggy Bank" + addChildDevice("wackford", "Quirky Wink Porkfolio", deviceId, null, [name: it.name, label: it.label, completedSetup: true]) + createWinkSubscription( it.subsPath, it.subsSuff ) + break + + case "eggtray": + log.debug "we have a Egg Minder" + addChildDevice("wackford", "Quirky Wink Eggtray", deviceId, null, [name: it.name, label: it.label, completedSetup: true]) + createWinkSubscription( it.subsPath, it.subsSuff ) + break + + case "cloud_clock": + log.debug "we have a Nimbus" + createNimbusChildren(it.data) //has sub-devices, so we call out to create kids + createWinkSubscription( it.subsPath, it.subsSuff ) + break + } + } + } + } +} + +def getDeviceList() +{ + log.debug "In getDeviceList" + + def deviceList = [:] + state.deviceDataArr = [] + + apiGet("/users/me/wink_devices") { response -> + response.data.data.each() { + if ( it.powerstrip_id ) { + deviceList["${it.powerstrip_id}"] = it.name + state.deviceDataArr.push(['name' : it.name, + 'id' : it.powerstrip_id, + 'type' : "powerstrip", + 'serial' : it.serial, + 'data' : it, + 'subsSuff': "/powerstripCallback", + 'subsPath': "/powerstrips/${it.powerstrip_id}/subscriptions" + ]) + } + + /* stubbing out these out for later release + if ( it.sensor_pod_id ) { + deviceList["${it.sensor_pod_id}"] = it.name + state.deviceDataArr.push(['name' : it.name, + 'id' : it.sensor_pod_id, + 'type' : "sensor_pod", + 'serial' : it.serial, + 'data' : it, + 'subsSuff': "/sensor_podCallback", + 'subsPath': "/sensor_pods/${it.sensor_pod_id}/subscriptions" + + ]) + } + + if ( it.piggy_bank_id ) { + deviceList["${it.piggy_bank_id}"] = it.name + state.deviceDataArr.push(['name' : it.name, + 'id' : it.piggy_bank_id, + 'type' : "piggy_bank", + 'serial' : it.serial, + 'data' : it, + 'subsSuff': "/piggy_bankCallback", + 'subsPath': "/piggy_banks/${it.piggy_bank_id}/subscriptions" + ]) + } + if ( it.cloud_clock_id ) { + deviceList["${it.cloud_clock_id}"] = it.name + state.deviceDataArr.push(['name' : it.name, + 'id' : it.cloud_clock_id, + 'type' : "cloud_clock", + 'serial' : it.serial, + 'data' : it, + 'subsSuff': "/cloud_clockCallback", + 'subsPath': "/cloud_clocks/${it.cloud_clock_id}/subscriptions" + ]) + } + if ( it.eggtray_id ) { + deviceList["${it.eggtray_id}"] = it.name + state.deviceDataArr.push(['name' : it.name, + 'id' : it.eggtray_id, + 'type' : "eggtray", + 'serial' : it.serial, + 'data' : it, + 'subsSuff': "/eggtrayCallback", + 'subsPath': "/eggtrays/${it.eggtray_id}/subscriptions" + ]) + } */ + + + } + } + return deviceList +} + +private removeChildDevices(delete) +{ + log.debug "In removeChildDevices" + + log.debug "deleting ${delete.size()} devices" + + delete.each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def uninstalled() +{ + log.debug "In uninstalled" + + removeWinkSubscriptions() + + removeChildDevices(getChildDevices()) +} + +def updateWinkSubscriptions() +{ //since we don't know when wink subscription dies, we'll delete and recreate on every poll + log.debug "In updateWinkSubscriptions" + + state.deviceDataArr.each() { + if (it.subsPath) { + def path = it.subsPath + def suffix = it.subsSuff + apiGet(it.subsPath) { response -> + response.data.data.each { + if ( it.subscription_id ) { + deleteWinkSubscription(path + "/", it.subscription_id) + createWinkSubscription(path, suffix) + } + } + } + } + } +} + +def createWinkSubscription(path, suffix) +{ + log.debug "In createWinkSubscription" + + def callbackUrl = buildCallbackUrl(suffix) + + httpPostJson([ + uri : apiUrl(), + path: path, + body: ['callback': callbackUrl], + headers : ['Authorization' : 'Bearer ' + state.vendorAccessToken] + ],) + { response -> + log.debug "Created subscription ID ${response.data.data.subscription_id}" + } +} + +def deleteWinkSubscription(path, subscriptionId) +{ + log.debug "Deleting the wink subscription ${subscriptionId}" + + httpDelete([ + uri : apiUrl(), + path: path + subscriptionId, + headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] + ],) + { response -> + log.debug "Subscription ${subscriptionId} deleted" + } +} + + +def removeWinkSubscriptions() +{ + log.debug "In removeSubscriptions" + + try { + state.deviceDataArr.each() { + if (it.subsPath) { + def path = it.subsPath + apiGet(it.subsPath) { response -> + response.data.data.each { + if ( it.subscription_id ) { + deleteWinkSubscription(path + "/", it.subscription_id) + } + } + } + } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.warn "Caught HttpResponseException: $e, with status: ${e.statusCode}" + } +} + +def buildCallbackUrl(suffix) +{ + log.debug "In buildRedirectUrl" + + def serverUrl = getServerUrl() + return serverUrl + "/api/token/${state.accessToken}/smartapps/installations/${app.id}" + suffix +} + +def createChildDevice(deviceFile, dni, name, label) +{ + log.debug "In createChildDevice" + + try { + def existingDevice = getChildDevice(dni) + if(!existingDevice) { + log.debug "Creating child" + def childDevice = addChildDevice("wackford", deviceFile, dni, null, [name: name, label: label, completedSetup: true]) + } else { + log.debug "Device $dni already exists" + } + } catch (e) { + log.error "Error creating device: ${e}" + } + +} + +def listDevices() +{ + log.debug "In listDevices" + + //login() + + def devices = getDeviceList() + log.debug "Device List = ${devices}" + + dynamicPage(name: "listDevices", title: "Choose devices", install: true) { + section("Devices") { + input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices + } + } +} + +def apiGet(String path, Closure callback) +{ + httpGet([ + uri : apiUrl(), + path : path, + headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] + ],) + { + response -> + callback.call(response) + } +} + +def apiPut(String path, cmd, Closure callback) +{ + httpPutJson([ + uri : apiUrl(), + path: path, + body: cmd, + headers : [ 'Authorization' : 'Bearer ' + state.vendorAccessToken ] + ],) + + { + response -> + callback.call(response) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + //initialize() + listDevices() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + //unsubscribe() + //unschedule() + initialize() + + listDevices() +} + + +def poll(childDevice) +{ + log.debug "In poll" + log.debug childDevice + + //login() + + def dni = childDevice.device.deviceNetworkId + + log.debug dni + + def deviceType = null + + state.deviceDataArr.each() { + if (it.id == dni) { + deviceType = it.type + } + } + + log.debug "device type is: ${deviceType}" + + switch(deviceType) { //outlets are polled in unique method not here + + case "sensor_pod": + log.debug "Polling sensor_pod" + getSensorPodUpdate(childDevice) + log.debug "sensor pod status updated" + break + + case "piggy_bank": + log.debug "Polling piggy_bank" + getPiggyBankUpdate(childDevice) + log.debug "piggy bank status updated" + break + + case "eggtray": + log.debug "Polling eggtray" + getEggtrayUpdate(childDevice) + log.debug "eggtray status updated" + break + + } + updateWinkSubscriptions() +} + +def cToF(temp) { + return temp * 1.8 + 32 +} + +def fToC(temp) { + return (temp - 32) / 1.8 +} + +def dollarize(int money) +{ + def value = money.toString() + + if ( value.length() == 1 ) { + value = "00" + value + } + + if ( value.length() == 2 ) { + value = "0" + value + } + + def newval = value.substring(0, value.length() - 2) + "." + value.substring(value.length()-2, value.length()) + value = newval + + def pattern = "\$0.00" + def moneyform = new DecimalFormat(pattern) + String output = moneyform.format(value.toBigDecimal()) + + return output +} + +def debugEvent(message, displayEvent) { + + def results = [ + name: "appdebug", + descriptionText: message, + displayed: displayEvent + ] + log.debug "Generating AppDebug Event: ${results}" + sendEvent (results) + +} + +///////////////////////////////////////////////////////////////////////// +// START NIMBUS SPECIFIC CODE HERE +///////////////////////////////////////////////////////////////////////// +def createNimbusChildren(deviceData) +{ + log.debug "In createNimbusChildren" + + def nimbusName = deviceData.name + def deviceFile = "Quirky-Wink-Nimbus" + def index = 1 + deviceData.dials.each { + log.debug "creating dial device for ${it.dial_id}" + def dialName = "Dial ${index}" + def dialLabel = "${nimbusName} ${dialName}" + createChildDevice( deviceFile, it.dial_id, dialName, dialLabel ) + index++ + } +} + +def cloud_clockEventHandler() +{ + log.debug "In Nimbus Event Handler..." + + def json = request.JSON + def dials = json.dials + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html + + if ( dials ) { + dials.each() { + def childDevice = getChildDevice(it.dial_id) + childDevice?.sendEvent( name : "dial", value : it.label , unit : "" ) + childDevice?.sendEvent( name : "info", value : it.name , unit : "" ) + } + } +} + +def pollNimbus(dni) +{ + + log.debug "In pollNimbus using dni # ${dni}" + + //login() + + def dials = null + + apiGet("/users/me/wink_devices") { response -> + + response.data.data.each() { + if (it.cloud_clock_id ) { + log.debug "Found Nimbus #" + it.cloud_clock_id + dials = it.dials + //log.debug dials + } + } + } + + if ( dials ) { + dials.each() { + def childDevice = getChildDevice(it.dial_id) + + childDevice?.sendEvent( name : "dial", value : it.label , unit : "" ) + childDevice?.sendEvent( name : "info", value : it.name , unit : "" ) + + //Change the tile/icon to what info is being displayed + switch(it.name) { + case "Weather": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-weather") + break + case "Traffic": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-traffic") + break + case "Time": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-time") + break + case "Twitter": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-twitter") + break + case "Calendar": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-calendar") + break + case "Email": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-mail") + break + case "Facebook": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-facebook") + break + case "Instagram": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-instagram") + break + case "Fitbit": + childDevice?.setIcon("dial", "dial", "st.quirky.nimbus.quirky-nimbus-fitbit") + break + case "Egg Minder": + childDevice?.setIcon("dial", "dial", "st.quirky.egg-minder.quirky-egg-device") + break + case "Porkfolio": + childDevice?.setIcon("dial", "dial", "st.quirky.porkfolio.quirky-porkfolio-side") + break + } + childDevice.save() + } + } + return +} + +///////////////////////////////////////////////////////////////////////// +// START EGG TRAY SPECIFIC CODE HERE +///////////////////////////////////////////////////////////////////////// +def getEggtrayUpdate(childDevice) +{ + log.debug "In getEggtrayUpdate" + + apiGet("/eggtrays/" + childDevice.device.deviceNetworkId) { response -> + + def data = response.data.data + def freshnessPeriod = data.freshness_period + def trayName = data.name + log.debug data + + int totalEggs = 0 + int oldEggs = 0 + + def now = new Date() + def nowUnixTime = now.getTime()/1000 + + data.eggs.each() { it -> + if (it != 0) + { + totalEggs++ + + def eggArriveDate = it + def eggStaleDate = eggArriveDate + freshnessPeriod + if ( nowUnixTime > eggStaleDate ){ + oldEggs++ + } + } + } + + int freshEggs = totalEggs - oldEggs + + if ( oldEggs > 0 ) { + childDevice?.sendEvent(name:"inventory",value:"haveBadEgg") + def msg = "${trayName} says: " + msg+= "Did you know that all it takes is one bad egg? " + msg+= "And it looks like I found one.\n\n" + msg+= "You should probably run an Egg Report before you use any eggs." + sendNotificationEvent(msg) + } + if ( totalEggs == 0 ) { + childDevice?.sendEvent(name:"inventory",value:"noEggs") + sendNotificationEvent("${trayName} says:\n'Oh no, I'm out of eggs!'") + sendNotificationEvent(msg) + } + if ( (freshEggs == totalEggs) && (totalEggs != 0) ) { + childDevice?.sendEvent(name:"inventory",value:"goodEggs") + } + childDevice?.sendEvent( name : "totalEggs", value : totalEggs , unit : "" ) + childDevice?.sendEvent( name : "freshEggs", value : freshEggs , unit : "" ) + childDevice?.sendEvent( name : "oldEggs", value : oldEggs , unit : "" ) + } +} + +def runEggReport(childDevice) +{ + apiGet("/eggtrays/" + childDevice.device.deviceNetworkId) { response -> + + def data = response.data.data + def trayName = data.name + def freshnessPeriod = data.freshness_period + def now = new Date() + def nowUnixTime = now.getTime()/1000 + + def eggArray = [] + + def i = 0 + + data.eggs.each() { it -> + if (it != 0 ) { + def eggArriveDate = it + def eggStaleDate = eggArriveDate + freshnessPeriod + if ( nowUnixTime > eggStaleDate ){ + eggArray.push("Bad ") + } else { + eggArray.push("Good ") + } + } else { + eggArray.push("Empty") + } + i++ + } + + def msg = " Egg Report for ${trayName}\n\n" + msg+= "#7:${eggArray[6]} #14:${eggArray[13]}\n" + msg+= "#6:${eggArray[5]} #13:${eggArray[12]}\n" + msg+= "#5:${eggArray[4]} #12:${eggArray[11]}\n" + msg+= "#4:${eggArray[3]} #11:${eggArray[10]}\n" + msg+= "#3:${eggArray[2]} #10:${eggArray[9]}\n" + msg+= "#2:${eggArray[1]} #9:${eggArray[8]}\n" + msg+= "#1:${eggArray[0]} #8:${eggArray[7]}\n" + msg+= " +\n" + msg+= " ===\n" + msg+= " ===" + + sendNotificationEvent(msg) + } +} + +def eggtrayEventHandler() +{ + log.debug "In eggtrayEventHandler..." + + def json = request.JSON + def dni = getChildDevice(json.eggtray_id) + + log.debug "event received from ${dni}" + + poll(dni) //sometimes events are stale, poll for all latest states + + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html +} + +///////////////////////////////////////////////////////////////////////// +// START PIGGY BANK SPECIFIC CODE HERE +///////////////////////////////////////////////////////////////////////// +def getPiggyBankUpdate(childDevice) +{ + apiGet("/piggy_banks/" + childDevice.device.deviceNetworkId) { response -> + def status = response.data.data + def alertData = status.triggers + + if (( alertData.enabled ) && ( state.lastCheckTime )) { + if ( alertData.triggered_at[0].toInteger() > state.lastCheckTime ) { + childDevice?.sendEvent(name:"acceleration",value:"active",unit:"") + } else { + childDevice?.sendEvent(name:"acceleration",value:"inactive",unit:"") + } + } + + childDevice?.sendEvent(name:"goal",value:dollarize(status.savings_goal),unit:"") + + childDevice?.sendEvent(name:"balance",value:dollarize(status.balance),unit:"") + + def now = new Date() + def longTime = now.getTime()/1000 + state.lastCheckTime = longTime.toInteger() + } +} + +def piggy_bankEventHandler() +{ + log.debug "In piggy_bankEventHandler..." + + def json = request.JSON + def dni = getChildDevice(json.piggy_bank_id) + + log.debug "event received from ${dni}" + + poll(dni) //sometimes events are stale, poll for all latest states + + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html +} + +///////////////////////////////////////////////////////////////////////// +// START SENSOR POD SPECIFIC CODE HERE +///////////////////////////////////////////////////////////////////////// +def getSensorPodUpdate(childDevice) +{ + apiGet("/sensor_pods/" + childDevice.device.deviceNetworkId) { response -> + def status = response.data.data.last_reading + + status.loudness ? childDevice?.sendEvent(name:"sound",value:"active",unit:"") : + childDevice?.sendEvent(name:"sound",value:"inactive",unit:"") + + status.brightness ? childDevice?.sendEvent(name:"light",value:"active",unit:"") : + childDevice?.sendEvent(name:"light",value:"inactive",unit:"") + + status.vibration ? childDevice?.sendEvent(name:"acceleration",value:"active",unit:"") : + childDevice?.sendEvent(name:"acceleration",value:"inactive",unit:"") + + status.external_power ? childDevice?.sendEvent(name:"powerSource",value:"powered",unit:"") : + childDevice?.sendEvent(name:"powerSource",value:"battery",unit:"") + + childDevice?.sendEvent(name:"humidity",value:status.humidity,unit:"") + + childDevice?.sendEvent(name:"battery",value:(status.battery * 100).toInteger(),unit:"") + + childDevice?.sendEvent(name:"temperature",value:cToF(status.temperature),unit:"F") + } +} + +def sensor_podEventHandler() +{ + log.debug "In sensor_podEventHandler..." + + def json = request.JSON + //log.debug json + def dni = getChildDevice(json.sensor_pod_id) + + log.debug "event received from ${dni}" + + poll(dni) //sometimes events are stale, poll for all latest states + + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html +} + +///////////////////////////////////////////////////////////////////////// +// START POWERSTRIP SPECIFIC CODE HERE +///////////////////////////////////////////////////////////////////////// + +def powerstripEventHandler() +{ + log.debug "In Powerstrip Event Handler..." + + def json = request.JSON + def outlets = json.outlets + + outlets.each() { + def dni = getChildDevice(it.outlet_id) + pollOutlet(dni) //sometimes events are stale, poll for all latest states + } + + def html = """{"code":200,"message":"OK"}""" + render contentType: 'application/json', data: html +} + +def pollOutlet(childDevice) +{ + log.debug "In pollOutlet" + + //login() + + log.debug "Polling powerstrip" + apiGet("/outlets/" + childDevice.device.deviceNetworkId) { response -> + def data = response.data.data + data.powered ? childDevice?.sendEvent(name:"switch",value:"on") : + childDevice?.sendEvent(name:"switch",value:"off") + } +} + +def on(childDevice) +{ + //login() + + apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : true]) { response -> + def data = response.data.data + log.debug "Sending 'on' to device" + } +} + +def off(childDevice) +{ + //login() + + apiPut("/outlets/" + childDevice.device.deviceNetworkId, [powered : false]) { response -> + def data = response.data.data + log.debug "Sending 'off' to device" + } +} + +def createPowerstripChildren(deviceData) +{ + log.debug "In createPowerstripChildren" + + def powerstripName = deviceData.name + def deviceFile = "Quirky Wink Powerstrip" + + deviceData.outlets.each { + createChildDevice( deviceFile, it.outlet_id, it.name, "$powerstripName ${it.name}" ) + } +} + +private Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +private Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +private List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} + diff --git a/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy b/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy new file mode 100644 index 00000000000..9f0e5a7f17f --- /dev/null +++ b/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy @@ -0,0 +1,662 @@ +/** + * TCP Bulbs (Connect) + * + * Copyright 2014 Todd Wackford + * + * 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. + * + */ + +import java.security.MessageDigest; + +private apiUrl() { "https://tcp.greenwavereality.com/gwr/gop.php?" } + +definition( + name: "Tcp Bulbs (Connect)", + namespace: "wackford", + author: "SmartThings", + description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png" +) + + +preferences { + def msg = """Tap 'Next' after you have entered in your TCP Mobile remote credentials. + +Once your credentials are accepted, SmartThings will scan your TCP installation for Bulbs.""" + + page(name: "selectDevices", title: "Connect Your TCP Lights to SmartThings", install: false, uninstall: true, nextPage: "chooseBulbs") { + section("TCP Connected Remote Credentials") { + input "username", "text", title: "Enter TCP Remote Email/UserName", required: true + input "password", "password", title: "Enter TCP Remote Password", required: true + paragraph msg + } + } + + page(name: "chooseBulbs", title: "Choose Bulbs to Control With SmartThings", content: "initialize") +} + +def installed() { + debugOut "Installed with settings: ${settings}" + + unschedule() + unsubscribe() + + setupBulbs() + + def cron = "0 11 23 * * ?" + log.debug "schedule('$cron', syncronizeDevices)" + schedule(cron, syncronizeDevices) +} + +def updated() { + debugOut "Updated with settings: ${settings}" + + unschedule() + + setupBulbs() + + def cron = "0 11 23 * * ?" + log.debug "schedule('$cron', syncronizeDevices)" + schedule(cron, syncronizeDevices) +} + +def uninstalled() +{ + unschedule() //in case we have hanging runIn()'s +} + +private removeChildDevices(delete) +{ + debugOut "deleting ${delete.size()} bulbs" + debugOut "deleting ${delete}" + delete.each { + deleteChildDevice(it.device.deviceNetworkId) + } +} + +def uninstallFromChildDevice(childDevice) +{ + def errorMsg = "uninstallFromChildDevice was called and " + if (!settings.selectedBulbs) { + debugOut errorMsg += "had empty list passed in" + return + } + + def dni = childDevice.device.deviceNetworkId + + if ( !dni ) { + debugOut errorMsg += "could not find dni of device" + return + } + + def newDeviceList = settings.selectedBulbs - dni + app.updateSetting("selectedBulbs", newDeviceList) + + debugOut errorMsg += "completed succesfully" +} + + +def setupBulbs() { + debugOut "In setupBulbs" + + def bulbs = state.devices + def deviceFile = "TCP Bulb" + + selectedBulbs.each { did -> + //see if this is a selected bulb and install it if not already + def d = getChildDevice(did) + + if(!d) { + def newBulb = bulbs.find { (it.did) == did } + d = addChildDevice("wackford", deviceFile, did, null, [name: "${newBulb?.name}", label: "${newBulb?.name}", completedSetup: true]) + + /*if ( isRoom(did) ) { //change to the multi light group icon for a room device + d.setIcon("switch", "on", "st.lights.multi-light-bulb-on") + d.setIcon("switch", "off", "st.lights.multi-light-bulb-off") + d.save() + }*/ + + } else { + debugOut "We already added this device" + } + } + + // Delete any that are no longer in settings + def delete = getChildDevices().findAll { !selectedBulbs?.contains(it.deviceNetworkId) } + removeChildDevices(delete) + + //we want to ensure syncronization between rooms and bulbs + //syncronizeDevices() +} + +def initialize() { + + atomicState.token = "" + + getToken() + + if ( atomicState.token == "error" ) { + return dynamicPage(name:"chooseBulbs", title:"TCP Login Failed!\r\nTap 'Done' to try again", nextPage:"", install:false, uninstall: false) { + section("") {} + } + } else { + "we're good to go" + debugOut "We have Token." + } + + //getGatewayData() //we really don't need anything from the gateway + + deviceDiscovery() + + def options = devicesDiscovered() ?: [] + + def msg = """Tap 'Done' after you have selected the desired devices.""" + + return dynamicPage(name:"chooseBulbs", title:"TCP and SmartThings Connected!", nextPage:"", install:true, uninstall: true) { + section("Tap Below to View Device List") { + input "selectedBulbs", "enum", required:false, title:"Select Bulb/Fixture", multiple:true, options:options + paragraph msg + } + } +} + +def deviceDiscovery() { + def data = "1${atomicState.token}" + + def Params = [ + cmd: "RoomGetCarousel", + data: "${data}", + fmt: "json" + ] + + def cmd = toQueryString(Params) + + def rooms = "" + + apiPost(cmd) { response -> + rooms = response.data.gip.room + } + + debugOut "rooms data = ${rooms}" + + def devices = [] + def bulbIndex = 1 + def lastRoomName = null + def deviceList = [] + + if ( rooms[1] == null ) { + def roomId = rooms.rid + def roomName = rooms.name + devices = rooms.device + if ( devices[1] != null ) { + debugOut "Room Device Data: did:${roomId} roomName:${roomName}" + //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"] + devices.each({ + debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" + deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] + }) + } else { + debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" + deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] + } + } else { + rooms.each({ + devices = it.device + def roomName = it.name + if ( devices[1] != null ) { + def roomId = it?.rid + debugOut "Room Device Data: did:${roomId} roomName:${roomName}" + //deviceList += ["name" : "${roomName}", "did" : "${roomId}", "type" : "room"] + devices.each({ + debugOut "Bulb Device Data: did:${it?.did} room:${roomName} BulbName:${it?.name}" + deviceList += ["name" : "${roomName} ${it?.name}", "did" : "${it?.did}", "type" : "bulb"] + }) + } else { + debugOut "Bulb Device Data: did:${devices?.did} room:${roomName} BulbName:${devices?.name}" + deviceList += ["name" : "${roomName} ${devices?.name}", "did" : "${devices?.did}", "type" : "bulb"] + } + }) + } + devices = ["devices" : deviceList] + state.devices = devices.devices +} + +Map devicesDiscovered() { + def devices = state.devices + def map = [:] + if (devices instanceof java.util.Map) { + devices.each { + def value = "${it?.name}" + def key = it?.did + map["${key}"] = value + } + } else { //backwards compatable + devices.each { + def value = "${it?.name}" + def key = it?.did + map["${key}"] = value + } + } + map +} + +def getGatewayData() { + debugOut "In getGatewayData" + + def data = "1${atomicState.token}" + + def qParams = [ + cmd: "GatewayGetInfo", + data: "${data}", + fmt: "json" + ] + + def cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "the gateway reponse is ${response.data.gip.gateway}" + } + +} + +def getToken() { + + atomicState.token = "" + + if (password) { + def hashedPassword = generateMD5(password) + + def data = "1${username}${hashedPassword}" + + def qParams = [ + cmd : "GWRLogin", + data: "${data}", + fmt : "json" + ] + + def cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + def status = response.data.gip.rc + + //sendNotificationEvent("Get token status ${status}") + + if (status != "200") {//success code = 200 + def errorText = response.data.gip.error + debugOut "Error logging into TCP Gateway. Error = ${errorText}" + atomicState.token = "error" + } else { + atomicState.token = response.data.gip.token + } + } + } else { + log.warn "Unable to log into TCP Gateway. Error = Password is null" + atomicState.token = "error" + } +} + +def apiPost(String data, Closure callback) { + //debugOut "In apiPost with data: ${data}" + def params = [ + uri: apiUrl(), + body: data + ] + + httpPost(params) { + response -> + def rc = response.data.gip.rc + + if ( rc == "200" ) { + debugOut ("Return Code = ${rc} = Command Succeeded.") + callback.call(response) + + } else if ( rc == "401" ) { + debugOut "Return Code = ${rc} = Error: User not logged in!" //Error code from gateway + log.debug "Refreshing Token" + getToken() + //callback.call(response) //stubbed out so getToken works (we had race issue) + + } else { + log.error "Return Code = ${rc} = Error!" //Error code from gateway + sendNotificationEvent("TCP Lighting is having Communication Errors. Error code = ${rc}. Check that TCP Gateway is online") + callback.call(response) + } + } +} + + +//this is not working. TCP power reporting is broken. Leave it here for future fix +def calculateCurrentPowerUse(deviceCapability, usePercentage) { + debugOut "In calculateCurrentPowerUse()" + + debugOut "deviceCapability: ${deviceCapability}" + debugOut "usePercentage: ${usePercentage}" + + def calcPower = usePercentage * 1000 + def reportPower = calcPower.round(1) as String + + debugOut "report power = ${reportPower}" + + return reportPower +} + +def generateSha256(String s) { + + MessageDigest digest = MessageDigest.getInstance("SHA-256") + digest.update(s.bytes) + new BigInteger(1, digest.digest()).toString(16).padLeft(40, '0') +} + +def generateMD5(String s) { + MessageDigest digest = MessageDigest.getInstance("MD5") + digest.update(s.bytes); + new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0') +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +def checkDevicesOnline(bulbs) { + debugOut "In checkDevicesOnline()" + + def onlineBulbs = [] + def thisBulb = [] + + bulbs.each { + def dni = it?.did + thisBulb = it + + def data = "1${atomicState.token}${dni}" + + def qParams = [ + cmd: "DeviceGetInfo", + data: "${data}", + fmt: "json" + ] + + def cmd = toQueryString(qParams) + + def bulbData = [] + + apiPost(cmd) { response -> + bulbData = response.data.gip + } + + if ( bulbData?.offline == "1" ) { + debugOut "${it?.name} is offline with offline value of ${bulbData?.offline}" + + } else { + debugOut "${it?.name} is online with offline value of ${bulbData?.offline}" + onlineBulbs += thisBulb + } + } + return onlineBulbs +} + +def syncronizeDevices() { + debugOut "In syncronizeDevices" + + def update = getChildDevices().findAll { selectedBulbs?.contains(it.deviceNetworkId) } + + update.each { + def dni = getChildDevice( it.deviceNetworkId ) + debugOut "dni = ${dni}" + + if (isRoom(dni)) { + pollRoom(dni) + } else { + poll(dni) + } + } +} + +boolean isRoom(dni) { + def device = state.devices.find() {(( it.type == 'room') && (it.did == "${dni}"))} +} + +boolean isBulb(dni) { + def device = state.devices.find() {(( it.type == 'bulb') && (it.did == "${dni}"))} +} + +def debugEvent(message, displayEvent) { + + def results = [ + name: "appdebug", + descriptionText: message, + displayed: displayEvent + ] + log.debug "Generating AppDebug Event: ${results}" + sendEvent (results) + +} + +def debugOut(msg) { + //log.debug msg + //sendNotificationEvent(msg) //Uncomment this for troubleshooting only +} + + +/************************************************************************** + Child Device Call In Methods + **************************************************************************/ +def on(childDevice) { + debugOut "On request from child device" + + def dni = childDevice.device.deviceNetworkId + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + data = "1$atomicState.token${dni}power1" + cmd = "RoomSendCommand" + } else { + data = "1$atomicState.token${dni}power1" + cmd = "DeviceSendCommand" + } + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "ON result: ${response.data}" + } + + //we want to ensure syncronization between rooms and bulbs + //runIn(2, "syncronizeDevices") +} + +def off(childDevice) { + debugOut "Off request from child device" + + def dni = childDevice.device.deviceNetworkId + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + data = "1$atomicState.token${dni}power0" + cmd = "RoomSendCommand" + } else { + data = "1$atomicState.token${dni}power0" + cmd = "DeviceSendCommand" + } + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "${response.data}" + } + + //we want to ensure syncronization between rooms and bulbs + //runIn(2, "syncronizeDevices") +} + +def setLevel(childDevice, value) { + debugOut "setLevel request from child device" + + def dni = childDevice.device.deviceNetworkId + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + data = "1${atomicState.token}${dni}level${value}" + cmd = "RoomSendCommand" + } else { + data = "1${atomicState.token}${dni}level${value}" + cmd = "DeviceSendCommand" + } + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + debugOut "${response.data}" + } + + //we want to ensure syncronization between rooms and bulbs + //runIn(2, "syncronizeDevices") +} + +// Really not called from child, but called from poll() if it is a room +def pollRoom(dni) { + debugOut "In pollRoom" + def data = "" + def cmd = "" + def roomDeviceData = [] + + data = "1${atomicState.token}${dni}name,power,control,status,state" + cmd = "RoomGetDevices" + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + roomDeviceData = response.data.gip + } + + debugOut "Room Data: ${roomDeviceData}" + + def totalPower = 0 + def totalLevel = 0 + def cnt = 0 + def onCnt = 0 //used to tally on/off states + + roomDeviceData.device.each({ + if ( getChildDevice(it.did) ) { + totalPower += it.other.bulbpower.toInteger() + totalLevel += it.level.toInteger() + onCnt += it.state.toInteger() + cnt += 1 + } + }) + + def avgLevel = totalLevel/cnt + def usingPower = totalPower * (avgLevel / 100) as float + def room = getChildDevice( dni ) + + //the device is a room but we use same type file + sendEvent( dni, [name: "setBulbPower",value:"${totalPower}"] ) //used in child device calcs + + //if all devices in room are on, room is on + if ( cnt == onCnt ) { // all devices are on + sendEvent( dni, [name: "switch",value:"on"] ) + sendEvent( dni, [name: "power",value:usingPower.round(1)] ) + + } else { //if any device in room is off, room is off + sendEvent( dni, [name: "switch",value:"off"] ) + sendEvent( dni, [name: "power",value:0.0] ) + } + + debugOut "Room Using Power: ${usingPower.round(1)}" +} + +def poll(childDevice) { + debugOut "In poll() with ${childDevice}" + + + def dni = childDevice.device.deviceNetworkId + + def bulbData = [] + def data = "" + def cmd = "" + + if ( isRoom(dni) ) { // this is a room, not a bulb + pollRoom(dni) + return + } + + data = "1${atomicState.token}${dni}" + cmd = "DeviceGetInfo" + + def qParams = [ + cmd: cmd, + data: "${data}", + fmt: "json" + ] + + cmd = toQueryString(qParams) + + apiPost(cmd) { response -> + bulbData = response.data.gip + } + + debugOut "This Bulbs Data Return = ${bulbData}" + + def bulb = getChildDevice( dni ) + + //set the devices power max setting to do calcs within the device type + if ( bulbData.other.bulbpower ) + sendEvent( dni, [name: "setBulbPower",value:"${bulbData.other.bulbpower}"] ) + + if (( bulbData.state == "1" ) && ( bulb?.currentValue("switch") != "on" )) + sendEvent( dni, [name: "switch",value:"on"] ) + + if (( bulbData.state == "0" ) && ( bulb?.currentValue("switch") != "off" )) + sendEvent( dni, [name: "switch",value:"off"] ) + + //if ( bulbData.level != bulb?.currentValue("level")) { + // sendEvent( dni, [name: "level",value: "${bulbData.level}"] ) + // sendEvent( dni, [name: "setLevel",value: "${bulbData.level}"] ) + //} + + if (( bulbData.state == "1" ) && ( bulbData.other.bulbpower )) { + def levelSetting = bulbData.level as float + def bulbPowerMax = bulbData.other.bulbpower as float + def calculatedPower = bulbPowerMax * (levelSetting / 100) + sendEvent( dni, [name: "power", value: calculatedPower.round(1)] ) + } + + if (( bulbData.state == "0" ) && ( bulbData.other.bulbpower )) + sendEvent( dni, [name: "power", value: 0.0] ) +}