├── smartapps ├── readme.md ├── pstuart │ ├── minimote-set-security-mode.src │ │ └── minimote-set-security-mode.groovy │ ├── test-push-notification-v2.src │ │ └── test-push-notification-v2.groovy │ ├── test-live-video-trigger.src │ │ └── test-live-video-trigger.groovy │ ├── live-code-friday-dynamic-page-function-call-issue.src │ │ └── live-code-friday-dynamic-page-function-call-issue.groovy │ ├── live-code-hvac-fan-run-away.src │ │ └── live-code-hvac-fan-run-away.groovy │ └── live-code-friday-virtual-device-manager.src │ │ └── live-code-friday-virtual-device-manager.groovy ├── GetPublicIP ├── set tstat to not away.groovy ├── dawn to dust motion dimmer ├── Control4 Send Command Test SmartApp.goovy ├── sample-webservice.groovy ├── smartthings │ └── laundry-monitor.src │ │ └── laundry-monitor.groovy ├── logger.groovy └── Ubi (Connect) ├── devicetypes ├── readme.md ├── cschwer │ └── http-post-example.src │ │ └── http-post-example.groovy ├── pstuart │ ├── unknown-zigbee-test-device.src │ │ └── unknown-zigbee-test-device.groovy │ ├── generic-switch.src │ │ └── generic-switch.groovy │ ├── generic-dimmer.src │ │ └── generic-dimmer.groovy │ ├── omnipresence.src │ │ └── omnipresence.groovy │ ├── insteon-moisture-local.src │ │ └── insteon-moisture-local.groovy │ ├── insteon-switch-local.src │ │ └── insteon-switch-local.groovy │ └── generic-camera.src │ │ └── generic-camera.groovy ├── ps │ ├── get-aws-ip.src │ │ └── get-aws-ip.groovy │ └── ps-cardaccess-zigbee-to-ir.src │ │ └── ps-cardaccess-zigbee-to-ir.groovy ├── smartslots.groovy ├── GE Link Bulb.groovy ├── smartthings │ ├── hue-bulb.src │ │ └── hue-bulb.groovy │ └── lifx-color-bulb.src │ │ └── lifx-color-bulb.groovy ├── Test Temp Slider.groovy ├── Get Ubi Sensors ├── Control4 Zigbee HA Dimmer.groovy ├── nest.groovy ├── smartsense multi + graph.groovy └── zenwithin │ └── zen-thermostat-new-ui.src │ └── zen-thermostat-new-ui.groovy ├── .gitignore └── README.md /smartapps/readme.md: -------------------------------------------------------------------------------- 1 | smartapps here 2 | -------------------------------------------------------------------------------- /devicetypes/readme.md: -------------------------------------------------------------------------------- 1 | devicetypes here 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Rakefile 2 | devicestypes/pstuart/ps-test-html-tile.src/ps-test-html-tile.groovy 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | smartthings 2 | =========== 3 | 4 | Place to hold my files and revisions on smartthings apps and devices 5 | 6 | 7 | How to set up a custom device 8 | 9 | 10 | 11 | You need IDE access. https://graph.api.smartthings.com/ 12 | 13 | go into my device types https://graph.api.smartthings.com/ide/devices 14 | 15 | click + New SmartDevice 16 | 17 | fill out just name, namespace and author and click create 18 | 19 | copy and paste the code 20 | 21 | Click the save button 22 | 23 | Click the publish button, for me 24 | 25 | Go to My Devices 26 | 27 | click + Add New Devices 28 | 29 | Give it the following: 30 | Name = anything you want 31 | device Network Id = unique id (should be the hex IP and hex port of the device but my code will auto insert that when you set the ip and take a photo) 32 | Type = Generic Camera Device 33 | Version = Published 34 | Location = your location 35 | hub = your hub 36 | group = what folder do you want the "thing" in, none is the default. 37 | 38 | then click create 39 | -------------------------------------------------------------------------------- /devicetypes/cschwer/http-post-example.src/http-post-example.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Post Example 3 | * 4 | * Copyright 2015 Charles Schwer 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | */ 15 | metadata { 16 | definition (name: "HTTP Post Example", namespace: "cschwer", author: "Charles Schwer") { 17 | } 18 | 19 | simulator { 20 | } 21 | 22 | tiles { 23 | } 24 | } 25 | 26 | // Parse events into attributes 27 | def parse(String description) { 28 | log.debug "Parsing '${description}'" 29 | def msg = parseLanMessage(description) 30 | 31 | log.debug "data ${msg.data}" 32 | log.debug "body ${msg.body}" 33 | log.debug "headers ${msg.headers}" 34 | } -------------------------------------------------------------------------------- /smartapps/pstuart/minimote-set-security-mode.src/minimote-set-security-mode.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * MiniMote Set Security Mode 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "MiniMote Set Security Mode", 18 | namespace: "pstuart", 19 | author: "Patrick Stuart", 20 | description: "MiniMote Set Security Mode", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 25 | 26 | 27 | preferences { 28 | section("MiniMote") { 29 | input "button", "capability.button" 30 | } 31 | } 32 | 33 | def installed() { 34 | log.debug "Installed with settings: ${settings}" 35 | 36 | initialize() 37 | } 38 | 39 | def updated() { 40 | log.debug "Updated with settings: ${settings}" 41 | 42 | unsubscribe() 43 | initialize() 44 | } 45 | 46 | def initialize() { 47 | // TODO: subscribe to attributes, devices, locations, etc. 48 | } 49 | 50 | -------------------------------------------------------------------------------- /devicetypes/pstuart/unknown-zigbee-test-device.src/unknown-zigbee-test-device.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Unknown Zigbee Test Device 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "Unknown Zigbee Test Device", namespace: "pstuart", author: "Patrick Stuart") { 18 | capability "Refresh" 19 | } 20 | 21 | simulator { 22 | // TODO: define status and reply messages here 23 | } 24 | 25 | tiles { 26 | // TODO: define your main and details tiles here 27 | } 28 | } 29 | 30 | // parse events into attributes 31 | def parse(String description) { 32 | log.debug "Parsing '${description}'" 33 | 34 | } 35 | 36 | // handle commands 37 | def refresh() { 38 | log.debug "Executing 'refresh'" 39 | // TODO: handle 'refresh' command 40 | getClusters(); 41 | } 42 | 43 | def getClusters() { 44 | log.debug "getClusters hit $device.deviceNetworkId please check full logs for response, may need a smartapp installed that subscribes to location parsing, like SHM, it will not be filtered to this device in the format like cp desc: ep_cnt:4, ep:01 C4 C5 C6" 45 | "zdo active 0x${device.deviceNetworkId}" 46 | } 47 | -------------------------------------------------------------------------------- /smartapps/pstuart/test-push-notification-v2.src/test-push-notification-v2.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Push Notification v2 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Test Push Notification v2", 18 | namespace: "pstuart", 19 | author: "Patrick Stuart", 20 | description: "Test Push Notification v2", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 25 | 26 | 27 | preferences { 28 | section("Send Notifications?") { 29 | input("recipients", "contact", title: "Send notifications to") 30 | } 31 | } 32 | 33 | def installed() { 34 | log.debug "Installed with settings: ${settings}" 35 | 36 | initialize() 37 | } 38 | 39 | def updated() { 40 | log.debug "Updated with settings: ${settings}" 41 | 42 | unsubscribe() 43 | initialize() 44 | } 45 | 46 | def initialize() { 47 | subscribe(app, appTouch) 48 | } 49 | 50 | def appTouch(evt) { 51 | sendNotificationToContacts("This is a test notification!", recipients) 52 | log.debug "Sent Notification" 53 | 54 | sendPush("This is a test sendPush notification.") 55 | log.debug "Sent sendPush Notification" 56 | } -------------------------------------------------------------------------------- /smartapps/pstuart/test-live-video-trigger.src/test-live-video-trigger.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Live Video Trigger 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Test Live Video Trigger", 18 | namespace: "pstuart", 19 | author: "Patrick Stuart", 20 | description: "Test Live Video Trigger", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 25 | 26 | 27 | preferences { 28 | section("Pick Camera to Turn on/off") { 29 | input "camera", "capability.switch" 30 | } 31 | } 32 | 33 | def installed() { 34 | log.debug "Installed with settings: ${settings}" 35 | 36 | initialize() 37 | } 38 | 39 | def updated() { 40 | log.debug "Updated with settings: ${settings}" 41 | 42 | unsubscribe() 43 | initialize() 44 | } 45 | 46 | def initialize() { 47 | subscribe(app, appTouch) 48 | } 49 | 50 | def appTouch(evt) { 51 | log.debug "app touch hit" 52 | def cameraState = camera.currentValue("switch") 53 | log.debug cameraState 54 | //def cameraRecord = camera.currentValue("record") 55 | //log.debug cameraRecord 56 | if (cameraState.startsWith("off")) { 57 | camera.on() 58 | } else 59 | { 60 | camera.off() //turn off 61 | } 62 | } -------------------------------------------------------------------------------- /devicetypes/pstuart/generic-switch.src/generic-switch.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic Switch 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "Generic Switch", namespace: "pstuart", author: "Patrick Stuart") { 18 | capability "Switch" 19 | } 20 | 21 | simulator { 22 | // TODO: define status and reply messages here 23 | } 24 | 25 | tiles { 26 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground:true) 27 | { 28 | state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#FF0000" 29 | state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00F208" 30 | } 31 | standardTile("toggle", "device.switch") 32 | { 33 | state "default", label:'Toggle', action: "toggle", icon: "st.secondary.refresh-icon" 34 | } 35 | main "switch" 36 | details(["switch","toggle"]) 37 | } 38 | } 39 | 40 | // parse events into attributes 41 | def parse(String description) { 42 | log.debug "Parsing '${description}'" 43 | // TODO: handle 'switch' attribute 44 | 45 | } 46 | 47 | // handle commands 48 | def on() { 49 | log.debug "Executing 'on'" 50 | // TODO: handle 'on' command 51 | sendEvent(name: "switch", value: "on") 52 | } 53 | 54 | def off() { 55 | log.debug "Executing 'off'" 56 | // TODO: handle 'off' command 57 | sendEvent(name: "switch", value: "off") 58 | } 59 | 60 | def toggle() { 61 | log.debug "Executing Toggle" 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /devicetypes/pstuart/generic-dimmer.src/generic-dimmer.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic Switch 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "Generic Dimmer", namespace: "pstuart", author: "Patrick Stuart") { 18 | capability "Switch" 19 | capability "Switch Level" 20 | } 21 | 22 | simulator { 23 | // TODO: define status and reply messages here 24 | } 25 | 26 | tiles { 27 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground:true) 28 | { 29 | state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#FF0000" 30 | state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00F208" 31 | } 32 | standardTile("toggle", "device.switch") 33 | { 34 | state "default", label:'Toggle', action: "toggle", icon: "st.secondary.refresh-icon" 35 | } 36 | main "switch" 37 | details(["switch","toggle"]) 38 | } 39 | } 40 | 41 | // parse events into attributes 42 | def parse(String description) { 43 | log.debug "Parsing '${description}'" 44 | // TODO: handle 'switch' attribute 45 | 46 | } 47 | 48 | // handle commands 49 | def on() { 50 | log.debug "Executing 'on'" 51 | // TODO: handle 'on' command 52 | sendEvent(name: "switch", value: "on") 53 | } 54 | 55 | def off() { 56 | log.debug "Executing 'off'" 57 | // TODO: handle 'off' command 58 | sendEvent(name: "switch", value: "off") 59 | } 60 | 61 | def toggle() { 62 | log.debug "Executing Toggle" 63 | } 64 | 65 | -------------------------------------------------------------------------------- /devicetypes/ps/get-aws-ip.src/get-aws-ip.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Get AWS IP 3 | * 4 | * Copyright 2015 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | 17 | preferences { 18 | } 19 | 20 | metadata { 21 | definition (name: "Get AWS IP", namespace: "ps", author: "patrick@patrickstuart.com") { 22 | capability "Polling" 23 | attribute "publicIp", "string" 24 | } 25 | 26 | simulator { 27 | } 28 | 29 | tiles(scale: 2) { 30 | valueTile("publicIp", "device.publicIp", inactiveLabel: false, decoration: "flat", width: 3, height: 2) { 31 | state "default", label:'${currentValue}', unit:"AWS IP" 32 | } 33 | standardTile("refresh", "device.poll", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 34 | state "default", action:"polling.poll", icon:"st.secondary.refresh" 35 | } 36 | valueTile("IPs", "device.ips", inactiveLabel: false, decoration: "flat", width: 6, height:7) { 37 | state "default", label:'${currentValue}', unit:"AWS IP List" 38 | } 39 | 40 | main "publicIp" 41 | details(["publicIp", "refresh", "IPs"]) 42 | } 43 | } 44 | 45 | def parse(String description) { 46 | log.debug "Parsing '${description}'" 47 | } 48 | 49 | // handle commands 50 | def poll() { 51 | log.debug "Executing 'poll'" 52 | getIP() 53 | } 54 | 55 | def getIP() { 56 | def path = "/st/ip.php" 57 | def params = [ 58 | uri: "http://valinor.net", 59 | path : path 60 | ] 61 | httpGet(params) { resp -> 62 | log.debug resp.data 63 | def publicip2 = resp.data.toString() 64 | sendEvent(name: 'publicIp', value: publicip2) 65 | def currentIps = device.currentValue("ips") 66 | if (!currentIps) { currentIps = "" } 67 | log.debug currentIps 68 | if (!currentIps.contains("$publicip2")) 69 | { 70 | currentIps += "\r\n" + publicip2 71 | } 72 | log.debug currentIps 73 | sendEvent(name: 'ips', value: currentIps) 74 | } 75 | } -------------------------------------------------------------------------------- /devicetypes/pstuart/omnipresence.src/omnipresence.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * OmniPresence 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "OmniPresence", namespace: "pstuart", author: "Patrick Stuart", oauth: true) { 18 | capability "Presence Sensor" 19 | capability "Sensor" 20 | } 21 | 22 | simulator { 23 | status "present": "presence: 1" 24 | status "not present": "presence: 0" 25 | } 26 | 27 | tiles { 28 | standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { 29 | state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") 30 | state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2") 31 | } 32 | main "presence" 33 | details "presence" 34 | } 35 | } 36 | 37 | // parse events into attributes 38 | def parse(String description) { 39 | log.debug "Parsing '${description}'" 40 | def msg = parseLanMessage(description) 41 | 42 | log.debug "data ${msg.data}" 43 | log.debug "body ${msg.body}" 44 | log.debug "headers ${msg.headers}" 45 | if ( msg.headers.toString().contains("get /?present=true")) 46 | { 47 | def results = [ 48 | name: "presence", 49 | value: "present", 50 | unit: null, 51 | linkText: "", 52 | descriptionText: "is Present", 53 | handlerName: null, 54 | isStateChange: true 55 | ] 56 | log.debug "Parse returned $results.descriptionText" 57 | return results 58 | } else if ( msg.headers.toString().contains("get /?present=false")) 59 | { 60 | def results = [ 61 | name: "presence", 62 | value: "not present", 63 | unit: null, 64 | linkText: "", 65 | descriptionText: "is Not Present", 66 | handlerName: null, 67 | isStateChange: true 68 | ] 69 | log.debug "Parse returned $results.descriptionText" 70 | return results 71 | } 72 | } -------------------------------------------------------------------------------- /devicetypes/smartslots.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * SmartSlots v1.0.0 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "SmartSlots v1.0", namespace: "pstuart", author: "Patrick Stuart") { 18 | command "spin" 19 | } 20 | 21 | simulator { 22 | } 23 | 24 | tiles { 25 | valueTile("slot1", "device.slot1", decoration:"flat") { 26 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:"" 27 | } 28 | valueTile("slot2", "device.slot2", decoration:"flat") { 29 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:"" 30 | } 31 | valueTile("slot3", "device.slot3", decoration:"flat") { 32 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:"" 33 | } 34 | valueTile("slotResult", "device.slotResult", decoration:"flat") { 35 | state "default", label:'${currentValue}', unit:"", backgroundColor:"#ffffff", icon:"" 36 | } 37 | standardTile("Spin", "device.button") { 38 | state "Spin", label:'${name}', action:"spin", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902' 39 | } 40 | 41 | main "Spin" 42 | details(["slot1", "slot2", "slot3", "slotResult", "Spin"]) 43 | } 44 | } 45 | 46 | // parse events into attributes 47 | def parse(String description) { 48 | log.debug "Parsing '${description}'" 49 | 50 | } 51 | 52 | def spin() { 53 | log.debug "Spin Hit" 54 | Random random = new Random() 55 | //log.debug random.nextInt(10)+1 56 | sendEvent(name: 'slot1', value: random.nextInt(10)+1) 57 | sendEvent(name: 'slot2', value: random.nextInt(10)+1) 58 | sendEvent(name: 'slot3', value: random.nextInt(10)+1) 59 | def slot1 = device.currentValue("slot1").toInteger() 60 | def slot2 = device.currentValue("slot2").toInteger() 61 | def slot3 = device.currentValue("slot3").toInteger() 62 | 63 | log.debug "$slot1 $slot2 $slot3 ${slot1 == slot2 && slot2 == slot3}" 64 | 65 | if (slot1 == slot2 && slot2 == slot3) { 66 | log.debug "Winner" 67 | sendEvent(name: 'slotResult', value: "Winner") 68 | } else { 69 | log.debug "Not a Winner" 70 | sendEvent(name: 'slotResult', value: "Not a Winner") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /smartapps/pstuart/live-code-friday-dynamic-page-function-call-issue.src/live-code-friday-dynamic-page-function-call-issue.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Live Code Friday Dynamic Page Function Call Issue 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Live Code Friday Dynamic Page Function Call Issue", 18 | namespace: "pstuart", 19 | author: "Patrick Stuart", 20 | description: "Live Code Friday Dynamic Page Function Call Issue", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 25 | 26 | 27 | preferences { 28 | page(name: "firstPage") 29 | page(name: "secondPage") 30 | } 31 | 32 | def firstPage() { 33 | dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) { 34 | section("Main Menu") { 35 | def test = testFunction("test1", "test2") 36 | paragraph "Testing A Functional Logging Issue from function $test" 37 | href(page: "secondPage", title: "Let's Add Devices!") 38 | } 39 | /* 40 | section("Later") { 41 | paragraph "More to come..." 42 | } 43 | */ 44 | 45 | } 46 | } 47 | 48 | def secondPage() { 49 | dynamicPage(name: "secondPage", title: "Where to now?", install: true, uninstall: true) { 50 | section("Main Menu 2") { 51 | def test = testFunction("test1", "test2") 52 | paragraph "Testing A Functional Logging Issue from function $test" 53 | href(page: "secondPage", title: "Let's Add Devices!") 54 | } 55 | /* 56 | section("Later") { 57 | paragraph "More to come..." 58 | } 59 | */ 60 | 61 | } 62 | } 63 | 64 | def testFunction(pOne, pTwo) { 65 | log.debug "This will not show up until after 'Done' is clicked" 66 | log.debug pOne 67 | log.debug pTwo 68 | return "test" 69 | } 70 | 71 | 72 | def installed() { 73 | log.debug "Installed with settings: ${settings}" 74 | 75 | initialize() 76 | } 77 | 78 | def updated() { 79 | log.debug "Updated with settings: ${settings}" 80 | 81 | unsubscribe() 82 | initialize() 83 | } 84 | 85 | def initialize() { 86 | // TODO: subscribe to attributes, devices, locations, etc. 87 | } 88 | 89 | // TODO: implement event handlers -------------------------------------------------------------------------------- /smartapps/GetPublicIP: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_GetPublicIP 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | 17 | preferences { 18 | } 19 | 20 | metadata { 21 | definition (name: "ps_GetPublicIP", namespace: "ps", author: "patrick@patrickstuart.com") { 22 | capability "Polling" 23 | 24 | 25 | attribute "publicIp", "string" 26 | } 27 | 28 | simulator { 29 | 30 | } 31 | 32 | tiles { 33 | valueTile("publicIp", "device.publicIp", inactiveLabel: false, decoration: "flat", columns:2) { 34 | state "default", label:'${currentValue}', unit:"Public IP" 35 | } 36 | standardTile("refresh", "device.poll", inactiveLabel: false, decoration: "flat") { 37 | state "default", action:"polling.poll", icon:"st.secondary.refresh" 38 | } 39 | 40 | 41 | main "publicIp" 42 | details(["publicIp", "refresh"]) 43 | } 44 | } 45 | 46 | // parse events into attributes 47 | def parse(String description) { 48 | log.debug "Parsing '${description}'" 49 | def map = stringToMap(description) 50 | def bodyString = new String(map.body.decodeBase64()) 51 | log.debug bodyString 52 | def body = new XmlSlurper().parseText(bodyString) 53 | log.debug body 54 | def publicip2 = body.toString().replace("Current IP CheckCurrent IP Address: ","") 55 | log.debug publicip2 56 | sendEvent(name: 'publicIp', value: publicip2) 57 | } 58 | 59 | // handle commands 60 | def poll() { 61 | log.debug "Executing 'poll'" 62 | login() 63 | } 64 | 65 | 66 | 67 | 68 | def login() { 69 | 70 | def method = "GET" 71 | def host = "216.146.38.70" 72 | def hosthex = convertIPtoHex(host) 73 | def porthex = convertPortToHex(80) 74 | device.deviceNetworkId = "$hosthex:$porthex" 75 | def headers = [:] 76 | headers.put("HOST", "$host:80") 77 | def path = "/" 78 | 79 | def hubAction = new physicalgraph.device.HubAction( 80 | method: method, 81 | path: path, 82 | headers: headers 83 | ) 84 | log.debug hubAction 85 | hubAction 86 | } 87 | 88 | 89 | 90 | private String convertIPtoHex(ipAddress) { 91 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 92 | log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 93 | return hex 94 | } 95 | private String convertPortToHex(port) { 96 | String hexport = port.toString().format( '%04x', port.toInteger() ) 97 | log.debug hexport 98 | return hexport 99 | } 100 | private Integer convertHexToInt(hex) { 101 | Integer.parseInt(hex,16) 102 | } 103 | private String convertHexToIP(hex) { 104 | log.debug("Convert hex to ip: $hex") 105 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 106 | } 107 | private getHostAddress() { 108 | def parts = device.deviceNetworkId.split(":") 109 | log.debug device.deviceNetworkId 110 | def ip = convertHexToIP(parts[0]) 111 | def port = convertHexToInt(parts[1]) 112 | return ip + ":" + port 113 | } 114 | -------------------------------------------------------------------------------- /smartapps/set tstat to not away.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Thermostat Away to Home 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Thermostat Away Mode Reset on Motion and Schedule", 18 | namespace: "ps", 19 | author: "patrick@patrickstuart.com", 20 | description: "Set the Nest thermostat (or any tstat) to home when motion is sensed or door is opened, etc.", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 24 | 25 | 26 | preferences { 27 | 28 | section("Thermostats") { 29 | input "tstat1", "capability.thermostat", title: "Which Tstats?", multiple: true 30 | } 31 | 32 | section("When there's movement..."){ 33 | input "motion1", "capability.motionSensor", title: "Where?", multiple: true, required: false 34 | } 35 | 36 | section("Schedule") { 37 | input(name: "days", type: "enum", title: "Allow Automatic Away/Not Away On These Days", description: "", 38 | required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]) 39 | 40 | input( name: "timeAway", title: "Turn On Away Time?", type: "time", required: false) 41 | 42 | input( name: "timeHome", title: "Turn Off Away Time?", type: "time", required: false) 43 | } 44 | 45 | 46 | 47 | } 48 | 49 | def installed() { 50 | log.debug "Installed with settings: ${settings}" 51 | initialize() 52 | } 53 | 54 | def updated() { 55 | log.debug "Updated with settings: ${settings}" 56 | unsubscribe() 57 | initialize() 58 | } 59 | 60 | def initialize() { 61 | subscribe(motion1, "motion.active", motionHandler) 62 | subscribe(location, modeChangeHandler) 63 | unschedule(changeModeHome) 64 | unschedule(changeModeAway) 65 | 66 | schedule(timeHome, changeModeHome) 67 | schedule(timeAway, changeModeAway) 68 | 69 | } 70 | 71 | def modeChangeHandler(evt) { 72 | log.debug "event at mode change event is $evt" 73 | if (evt.value == "Home") { 74 | log.debug "Mode changed to Home" 75 | setHome() 76 | } 77 | if (evt.value == "Away") { 78 | log.debug "Mode changed to Away" 79 | setAway() 80 | } 81 | } 82 | 83 | def changeModeHome(evt) { 84 | log.debug "change Mode Home Fired" 85 | def today = new Date().format("EEEE") 86 | //log.debug "today: ${today}, days: ${days}" 87 | 88 | if (!days || days.contains(today)) { 89 | log.debug "Set to Not Away on $today" 90 | setHome() 91 | } 92 | } 93 | 94 | def changeModeAway(evt) { 95 | log.debug "change Mode Away Fired" 96 | def today = new Date().format("EEEE") 97 | if (!days || days.contains(today)) { 98 | log.debug "Set to Away on $today" 99 | setAway() 100 | } 101 | } 102 | 103 | def motionHandler(evt) { 104 | log.debug "Motion detected, $evt Set to Not Away" 105 | setHome() 106 | } 107 | 108 | def setHome() { 109 | for (t in settings.tstat1) { 110 | if (tstat1[0].latestValue("presence") == "away") { 111 | log.debug "Setting tstat to here" 112 | t.present() 113 | } 114 | } 115 | } 116 | 117 | def setAway() { 118 | for (t in settings.tstat1) { 119 | if (tstat1[0].latestValue("presence") == "present") { 120 | log.debug "Setting tstat to away" 121 | t.away() 122 | } 123 | } 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /smartapps/dawn to dust motion dimmer: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_Dust to Dawn Motion Lights 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "ps_Dust to Dawn Motion Lights", 18 | namespace: "ps", 19 | author: "patrick@patrickstuart.com", 20 | description: "Turn lights on at Dust, motion bright, fade out no motion, turn off lights at Dawn...", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 24 | 25 | 26 | preferences { 27 | section("Select Dimmers you want to Use") { 28 | input "motions", "capability.motionSensor", title: "Motions", required: false, multiple: true 29 | input "switches", "capability.switchLevel", title: "Switches", required: false, multiple: true 30 | } 31 | section ("Zip code (optional, defaults to location coordinates)...") { 32 | input "zipCode", "text", required: false 33 | } 34 | } 35 | 36 | def installed() { 37 | log.debug "Installed with settings: ${settings}" 38 | initialize() 39 | } 40 | 41 | def updated() { 42 | log.debug "Updated with settings: ${settings}" 43 | unsubscribe() 44 | initialize() 45 | } 46 | 47 | def initialize() { 48 | subscribe(motions, "motion", handleMotionEvent) 49 | checkSun() 50 | } 51 | 52 | def checkSun() { 53 | def zip = settings.zip as String 54 | def sunInfo = getSunriseAndSunset(zipCode: zip) 55 | def current = now() 56 | if(sunInfo.sunrise.time > current || sunInfo.sunset.time < current) { 57 | state.sunMode = "SunsetMode" 58 | } 59 | else { 60 | state.sunMode = "SunriseMode" 61 | } 62 | log.info("Sunset: ${sunInfo.sunset.time}") 63 | log.info("Sunrise: ${sunInfo.sunrise.time}") 64 | log.info("Current: ${current}") 65 | log.info("sunMode: ${state.sunMode}") 66 | 67 | if(current < sunInfo.sunrise.time) { 68 | runIn(((sunInfo.sunrise.time - current) / 1000).toInteger(), setSunrise) 69 | } 70 | 71 | if(current < sunInfo.sunset.time) { 72 | runIn(((sunInfo.sunset.time - current) / 1000).toInteger(), setSunset) 73 | } 74 | schedule(timeTodayAfter(new Date(), "01:00", location.timeZone), checkSun) 75 | if (state.sunMode == "SunriseMode") 76 | { 77 | setSunrise() 78 | } 79 | else 80 | { 81 | setSunset() 82 | } 83 | } 84 | 85 | def handleMotionEvent(evt) { 86 | log.debug "Motion event Detected do something $evt" 87 | log.debug state 88 | state.motion = evt.value 89 | 90 | //testing force state.sunMode to SunsetMode 91 | //state.sunMode = "SunsetMode" 92 | 93 | if (state.sunMode == "SunsetMode" && state.motion == "active") { 94 | switches?.setLevel(100) 95 | state.Level = "99" 96 | log.debug "Set the switches to level 100" 97 | log.debug state 98 | 99 | //Change this to # of seconds to leave light on 100 | runIn(300, setSunset) 101 | } 102 | else 103 | { 104 | log.debug "it is after sunrise but before sunset, do nothing" 105 | //runIn(120, sunriseHandler) 106 | } 107 | } 108 | 109 | def setSunset() { 110 | log.debug "Sunset handler" 111 | switches?.setLevel(20) 112 | state.Level = "20" 113 | // if state.motion is false 114 | } 115 | 116 | def setSunrise() { 117 | log.info "Executing sunrise handler" 118 | switches?.setLevel(0) 119 | state.Level = "off" 120 | } 121 | -------------------------------------------------------------------------------- /devicetypes/pstuart/insteon-moisture-local.src/insteon-moisture-local.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Insteon Moisture (LOCAL) 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * Updated 1/4/15 by goldmichael@gmail.com 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at: 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 14 | * for the specific language governing permissions and limitations under the License. 15 | * 16 | */ 17 | metadata { 18 | definition (name: "Insteon Moisture (LOCAL)", namespace: "pstuart", author: "patrick@patrickstuart.com") { 19 | capability "Switch" 20 | capability "Sensor" 21 | capability "Actuator" 22 | capability "Water Sensor" 23 | capability "Polling" 24 | 25 | command "status" 26 | } 27 | 28 | preferences { 29 | input("InsteonIP", "string", title:"Insteon IP Address", description: "Please enter your Insteon Hub IP Address", defaultValue: "192.168.1.2", required: true, displayDuringSetup: true) 30 | input("InsteonPort", "string", title:"Insteon Port", description: "Please enter your Insteon Hub Port", defaultValue: 25105, required: true, displayDuringSetup: true) 31 | input("InsteonID", "string", title:"Device Insteon ID", description: "Please enter the devices Insteon ID - numbers only", defaultValue: "1E65F2", required: true, displayDuringSetup: true) 32 | input("InsteonHubUsername", "string", title:"Insteon Hub Username", description: "Please enter your Insteon Hub Username", defaultValue: "michael" , required: true, displayDuringSetup: true) 33 | input("InsteonHubPassword", "string", title:"Insteon Hub Password", description: "Please enter your Insteon Hub Password", defaultValue: "password" , required: true, displayDuringSetup: true) 34 | } 35 | 36 | simulator { 37 | 38 | } 39 | 40 | // UI tile definitions 41 | tiles { 42 | standardTile("water", "device.water", width: 2, height: 2, canChangeIcon: false) { 43 | state "dry", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 44 | state "wet", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 45 | } 46 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { 47 | state "battery", label:'${currentValue} battery', unit:"" 48 | } 49 | standardTile("refresh", "device.button", inactiveLabel: false, decoration: "flat") { 50 | state "refresh", action:"status", icon:"st.secondary.refresh" 51 | } 52 | 53 | main "water" 54 | details(["water", "battery", "refresh"]) 55 | } 56 | } 57 | 58 | def poll() { 59 | status() 60 | } 61 | 62 | def status() { 63 | log.debug "status hit" 64 | def host = InsteonIP 65 | 66 | def path = "/3?0262" + "${InsteonID}" + "0F19FF=I=3" 67 | log.debug "path is: $path" 68 | 69 | 70 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}" 71 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 72 | def headers = [:] //"HOST:" 73 | headers.put("HOST", "$host:$InsteonPort") 74 | headers.put("Authorization", userpass) 75 | 76 | try { 77 | def hubAction = new physicalgraph.device.HubAction( 78 | method: method, 79 | path: path, 80 | headers: headers 81 | ) 82 | } 83 | catch (Exception e) { 84 | log.debug "Hit Exception on $hubAction" 85 | log.debug e 86 | } 87 | 88 | } 89 | 90 | def parse(String description) { 91 | log.debug "Parsing $description" 92 | def map = stringToMap(description) 93 | 94 | if (description == "0") 95 | { 96 | sendEvent(name: "water", value: "wet") 97 | } 98 | if (description == "1") 99 | { 100 | sendEvent(name: "water", value: "dry") 101 | } 102 | if (description == "2") 103 | { 104 | sendEvent(name: "battery", value: "low") 105 | } 106 | if (description == "3") 107 | { 108 | sendEvent(name: "battery", value: "ok") 109 | } 110 | } -------------------------------------------------------------------------------- /smartapps/pstuart/live-code-hvac-fan-run-away.src/live-code-hvac-fan-run-away.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Live Code HVAC Fan Run Away 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Live Code HVAC Fan Run Away", 18 | namespace: "pstuart", 19 | author: "Patrick Stuart", 20 | description: "My wife's first one today smartapp request.", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 24 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 25 | oauth: true) 26 | 27 | 28 | preferences { 29 | section("Thermostats") { 30 | input "tstats", "capability.thermostat", title: "HVAC System", required:true, multiple: true 31 | 32 | } 33 | 34 | section("Set how long (in minutes) you want the fan to run, minimum of 5 minutes.") { 35 | input "fanmins", "number", title: "Minutes to Run Fan" 36 | } 37 | section ([mobileOnly:true]) { 38 | label title: "Assign a name", required: false 39 | mode title: "Set for specific mode(s)", required: false 40 | } 41 | } 42 | 43 | mappings { 44 | path("/html") {action: [GET: "html"]} 45 | path("/updates") {action: [GET: "updates"]} 46 | path("/:id/:command") {action: [GET: "deviceAction"]} 47 | } 48 | 49 | def installed() { 50 | log.debug "Installed with settings: ${settings}" 51 | 52 | initialize() 53 | } 54 | 55 | def updated() { 56 | log.debug "Updated with settings: ${settings}" 57 | 58 | unsubscribe() 59 | initialize() 60 | } 61 | 62 | def initialize() { 63 | // start the hvac fan 64 | // start a timer 65 | // at end of timer, stop hvac fan 66 | subscribe(app, startFan) 67 | startFan() 68 | log.debug "Fan Started" 69 | } 70 | 71 | def startFan(evt) { 72 | log.debug "startFan function hit" 73 | tstats*.fanOn() 74 | runIn(fanmins * 60, stopFan) 75 | } 76 | 77 | def stopFan(evt) { 78 | log.debug "stopFan function hit" 79 | unschedule() 80 | tstats*.fanAuto() 81 | log.debug "fan stopped" 82 | 83 | } 84 | 85 | def html() { 86 | state.updates = [] 87 | render contentType: "text/html", data: """ 88 | 89 | 90 | ${js()} 91 | 92 | 93 |
94 | 97 |
98 |
99 | ${sws()} 100 |
101 |
102 |
103 | 104 | 105 | """ 106 | } 107 | 108 | def sws() { 109 | def markup = "" 110 | tstats.each { 111 | markup = markup + """ 112 |
113 |
${it.displayName}
114 |
${it.id}
115 |
${it.currentValue("thermostatFanMode")}
116 |
Toggle
117 |
118 | """ 119 | } 120 | 121 | markup 122 | } 123 | 124 | 125 | def js() { """ 126 | 127 | 133 | """ 134 | } 135 | 136 | def deviceAction() { 137 | log.debug params.id 138 | log.debug params.command 139 | def sw = switches.find {it.id == params.id } 140 | if (sw.currentValue("thermostatFanMode") == "on") { 141 | sw.auto() 142 | render contentType: "text/html", data: """auto""" 143 | } 144 | else 145 | { 146 | sw.on() 147 | render contentType: "text/html", data: """on""" 148 | } 149 | 150 | 151 | } -------------------------------------------------------------------------------- /smartapps/Control4 Send Command Test SmartApp.goovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Control4 Send Command Test 3 | * 4 | * Copyright 2014 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | definition( 17 | name: "Control4 Send Command Test", 18 | namespace: "", 19 | author: "Patrick Stuart", 20 | description: "Testing sending a http request to Control4 controller", 21 | category: "My Apps", 22 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 23 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" 24 | ) 25 | 26 | 27 | preferences { 28 | section("When the door opens/closes...") { 29 | input "contact1", "capability.contactSensor" 30 | } 31 | section("When I touch the app, Send to server...") { 32 | input "server", "text", title: "Server IP", description: "Your HTTP Server IP", required: true 33 | input "command", "text", title: "Parameters", description: "Command to Send", required: true 34 | } 35 | } 36 | 37 | def installed() { 38 | log.debug "Installed with settings: ${settings}" 39 | subscribe(contact1.contact) 40 | subscribe(app, appTouch) 41 | initialize() 42 | } 43 | 44 | def updated() { 45 | log.debug "Updated with settings: ${settings}" 46 | unsubscribe() 47 | subscribe(contact1.contact) 48 | subscribe(app, appTouch) 49 | initialize() 50 | } 51 | 52 | def initialize() { 53 | subscribe(location, null, locationHandler, [filterEvents:false]) 54 | // TODO: subscribe to attributes, devices, locations, etc. 55 | } 56 | 57 | // TODO: implement event handlers 58 | 59 | 60 | def appTouch(evt) { 61 | log.debug "appTouch: $evt" 62 | sendHttp() 63 | //sendSoap() 64 | } 65 | 66 | //def contact(evt) 67 | //{ 68 | //def params = [ 69 | // uri: 'https://192.168.101.165:8080/Test', 70 | // body: [param1: myParam1, param2: myParam2] 71 | // ] 72 | 73 | // log.debug "Params -> $params" 74 | // httpPost(params) { 75 | // response -> 76 | // log.debug "Response Received: Status [$response.status]" 77 | // } 78 | //} 79 | 80 | def sendSoap() { 81 | 82 | def ip = "${settings.server}:5020" 83 | def deviceNetworkId = "C0A865A5:139C" 84 | def cmdc4 = '''''' 85 | log.debug("c4 command:" + cmdc4) 86 | sendHubCommand(new physicalgraph.device.HubAction("""GET """ + cmdc4 + """HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) 87 | 88 | log.debug("sent test command") 89 | log.debug location.hubs*.firmwareVersionString.findAll { it } 90 | 91 | } 92 | 93 | 94 | 95 | 96 | 97 | def sendHttp() { 98 | def ip = "${settings.server}:8080" 99 | def deviceNetworkId = "C0A865A5" 100 | 101 | sendHubCommand(new physicalgraph.device.HubAction("""GET /${settings.command} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) 102 | 103 | log.debug("sent test command") 104 | log.debug location.hubs*.firmwareVersionString.findAll { it } 105 | } 106 | 107 | 108 | def locationHandler(evt) { 109 | def description = evt.description 110 | def hub = evt?.hubId 111 | 112 | log.debug "cp desc: " + description 113 | //log.debug description 114 | //def headerString = new String(description.decodeBase64()) 115 | 116 | //log.debug("Count" + description.count(",")) 117 | if (description.count(",") > 4) 118 | { 119 | def bodyString = new String(description.split(',')[5].split(":")[1].decodeBase64()) 120 | log.debug(bodyString) 121 | } 122 | 123 | 124 | } 125 | 126 | private Long converIntToLong(ipAddress) { 127 | log.debug(ipAddress) 128 | long result = 0;; 129 | def parts = ipAddress.split("\\.") 130 | for (int i = 3; i >= 0; i--) { 131 | result |= (Long.parseLong(parts[3 - i]) << (i * 8)); 132 | } 133 | 134 | return result & 0xFFFFFFFF; 135 | } 136 | 137 | private String convertIPToHex(ipAddress) { 138 | return Long.toHexString(converIntToLong(ipAddress)); 139 | } 140 | 141 | private String getDeviceId() { 142 | def ip = convertIPToHex(settings.server) 143 | def port = Long.toHexString(Long.parseLong(settings.port)) 144 | return ip + ":0x" + port 145 | } 146 | -------------------------------------------------------------------------------- /devicetypes/GE Link Bulb.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_GE LinkBulb v2 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "ps_GE LinkBulb v2", namespace: "ps", author: "patrick@patrickstuart.com") { 18 | 19 | capability "Switch Level" 20 | capability "Actuator" 21 | capability "Switch" 22 | capability "Configuration" 23 | capability "Polling" 24 | capability "Refresh" 25 | capability "Sensor" 26 | 27 | fingerprint endpointId: "1", profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019" 28 | } 29 | 30 | // simulator metadata 31 | simulator { 32 | // status messages 33 | status "on": "on/off: 1" 34 | status "off": "on/off: 0" 35 | 36 | // reply messages 37 | reply "zcl on-off on": "on/off: 1" 38 | reply "zcl on-off off": "on/off: 0" 39 | } 40 | 41 | // UI tile definitions 42 | tiles { 43 | standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { 44 | state "off", label: '${name}', action: "switch.on", icon: "st.Lighting.light13", backgroundColor: "#ffffff" 45 | state "on", label: '${name}', action: "switch.off", icon: "st.Lighting.light11", backgroundColor: "#79b821" 46 | } 47 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { 48 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 49 | } 50 | controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { 51 | state "color", action:"setAdjustedColor" 52 | } 53 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { 54 | state "level", action:"switch level.setLevel" 55 | } 56 | valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { 57 | state "level", label: 'Level ${currentValue}%' 58 | } 59 | 60 | 61 | main(["switch"]) 62 | details(["switch", "levelSliderControl", "level", "refresh"]) 63 | } 64 | } 65 | 66 | // Parse incoming device messages to generate events 67 | def parse(String description) { 68 | log.trace description 69 | if (description?.startsWith("catchall:")) { 70 | //def msg = zigbee.parse(description) 71 | //log.trace msg 72 | //log.trace "data: $msg.data" 73 | if(description?.endsWith("0100")) 74 | { 75 | def result = createEvent(name: "switch", value: "on") 76 | log.debug "Parse returned ${result?.descriptionText}" 77 | return result 78 | } 79 | if(description?.endsWith("0000")) 80 | { 81 | def result = createEvent(name: "switch", value: "off") 82 | log.debug "Parse returned ${result?.descriptionText}" 83 | return result 84 | } 85 | } 86 | if (description?.startsWith("read attr")) { 87 | log.debug description[-2..-1] 88 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) 89 | 90 | sendEvent( name: "level", value: i ) 91 | } 92 | 93 | 94 | } 95 | 96 | def on() { 97 | // just assume it works for now 98 | log.debug "on()" 99 | sendEvent(name: "switch", value: "on") 100 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" 101 | } 102 | 103 | def off() { 104 | // just assume it works for now 105 | log.debug "off()" 106 | sendEvent(name: "switch", value: "off") 107 | "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 108 | } 109 | 110 | def refresh() { 111 | "st rattr 0x${device.deviceNetworkId} 1 6 0" 112 | "st rattr 0x${device.deviceNetworkId} 1 8 0" 113 | } 114 | 115 | def poll(){ 116 | log.debug "Poll is calling refresh" 117 | refresh() 118 | } 119 | 120 | def setLevel(value) { 121 | log.trace "setLevel($value)" 122 | def cmds = [] 123 | 124 | if (value == 0) { 125 | sendEvent(name: "switch", value: "off") 126 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" 127 | } 128 | else if (device.latestValue("switch") == "off") { 129 | sendEvent(name: "switch", value: "on") 130 | } 131 | 132 | sendEvent(name: "level", value: value) 133 | def level = new BigInteger(Math.round(value * 255 / 100).toString()).toString(16) 134 | cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" 135 | 136 | //log.debug cmds 137 | cmds 138 | } 139 | 140 | private getEndpointId() { 141 | new BigInteger(device.endpointId, 16).toString() 142 | } 143 | 144 | private hex(value, width=2) { 145 | def s = new BigInteger(Math.round(value).toString()).toString(16) 146 | while (s.size() < width) { 147 | s = "0" + s 148 | } 149 | s 150 | } 151 | 152 | private Integer convertHexToInt(hex) { 153 | Integer.parseInt(hex,16) 154 | } 155 | -------------------------------------------------------------------------------- /devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Hue Bulb 3 | * 4 | * Author: SmartThings 5 | */ 6 | // for the UI 7 | metadata { 8 | // Automatically generated. Make future change here. 9 | definition (name: "Hue Bulb", namespace: "smartthings", author: "SmartThings") { 10 | capability "Switch Level" 11 | capability "Actuator" 12 | capability "Color Control" 13 | capability "Switch" 14 | capability "Refresh" 15 | capability "Sensor" 16 | 17 | command "setAdjustedColor" 18 | command "reset" 19 | command "refresh" 20 | } 21 | 22 | simulator { 23 | // TODO: define status and reply messages here 24 | } 25 | 26 | tiles (scale: 2){ 27 | multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ 28 | tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { 29 | attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" 30 | attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" 31 | attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" 32 | attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" 33 | } 34 | tileAttribute ("device.level", key: "SLIDER_CONTROL") { 35 | attributeState "level", action:"switch level.setLevel" 36 | } 37 | tileAttribute ("device.color", key: "COLOR_CONTROL") { 38 | attributeState "color", action:"setAdjustedColor" 39 | } 40 | } 41 | 42 | standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 43 | state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" 44 | } 45 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 46 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 47 | } 48 | } 49 | 50 | main(["switch"]) 51 | details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"]) 52 | } 53 | 54 | // parse events into attributes 55 | def parse(description) { 56 | log.debug "parse() - $description" 57 | def results = [] 58 | def map = description 59 | if (description instanceof String) { 60 | log.debug "Hue Bulb stringToMap - ${map}" 61 | map = stringToMap(description) 62 | } 63 | if (map?.name && map?.value) { 64 | results << createEvent(name: "${map?.name}", value: "${map?.value}") 65 | } 66 | results 67 | } 68 | 69 | // handle commands 70 | def on() { 71 | log.trace parent.on(this) 72 | sendEvent(name: "switch", value: "on") 73 | } 74 | 75 | def off() { 76 | log.trace parent.off(this) 77 | sendEvent(name: "switch", value: "off") 78 | } 79 | 80 | def nextLevel() { 81 | def level = device.latestValue("level") as Integer ?: 0 82 | if (level <= 100) { 83 | level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer 84 | } 85 | else { 86 | level = 25 87 | } 88 | setLevel(level) 89 | } 90 | 91 | def setLevel(percent) { 92 | log.debug "Executing 'setLevel'" 93 | parent.setLevel(this, percent) 94 | sendEvent(name: "level", value: percent) 95 | } 96 | 97 | def setSaturation(percent) { 98 | log.debug "Executing 'setSaturation'" 99 | parent.setSaturation(this, percent) 100 | sendEvent(name: "saturation", value: percent) 101 | } 102 | 103 | def setHue(percent) { 104 | log.debug "Executing 'setHue'" 105 | parent.setHue(this, percent) 106 | sendEvent(name: "hue", value: percent) 107 | } 108 | 109 | def setColor(value) { 110 | log.debug "setColor: ${value}, $this" 111 | parent.setColor(this, value) 112 | if (value.hue) { sendEvent(name: "hue", value: value.hue)} 113 | if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} 114 | if (value.hex) { sendEvent(name: "color", value: value.hex)} 115 | if (value.level) { sendEvent(name: "level", value: value.level)} 116 | if (value.switch) { sendEvent(name: "switch", value: value.switch)} 117 | } 118 | 119 | def reset() { 120 | log.debug "Executing 'reset'" 121 | def value = [level:100, hex:"#90C638", saturation:56, hue:23] 122 | setAdjustedColor(value) 123 | parent.poll() 124 | } 125 | 126 | def setAdjustedColor(value) { 127 | if (value) { 128 | log.trace "setAdjustedColor: ${value}" 129 | def adjusted = value + [:] 130 | adjusted.hue = adjustOutgoingHue(value.hue) 131 | // Needed because color picker always sends 100 132 | adjusted.level = null 133 | setColor(adjusted) 134 | } 135 | } 136 | 137 | def refresh() { 138 | log.debug "Executing 'refresh'" 139 | parent.manualRefresh() 140 | } 141 | 142 | def adjustOutgoingHue(percent) { 143 | def adjusted = percent 144 | if (percent > 31) { 145 | if (percent < 63.0) { 146 | adjusted = percent + (7 * (percent -30 ) / 32) 147 | } 148 | else if (percent < 73.0) { 149 | adjusted = 69 + (5 * (percent - 62) / 10) 150 | } 151 | else { 152 | adjusted = percent + (2 * (100 - percent) / 28) 153 | } 154 | } 155 | log.info "percent: $percent, adjusted: $adjusted" 156 | adjusted 157 | } 158 | -------------------------------------------------------------------------------- /devicetypes/Test Temp Slider.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_TestTempSlider 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "ps_TestTempSlider", namespace: "ps", author: "patrick@patrickstuart.com") { 18 | capability "Polling" 19 | capability "Thermostat" 20 | capability "Temperature Measurement" 21 | } 22 | 23 | 24 | preferences { 25 | input("SliderMin", "Double", title: "Slider Min Adjust", description: "Set Slider Min") 26 | input("SliderMax", "Double", title: "Slider Max Adjust", description: "Set Slider Max") 27 | } 28 | 29 | 30 | simulator { 31 | // TODO: define status and reply messages here 32 | 33 | } 34 | 35 | tiles { 36 | // TODO: define your main and details tiles here 37 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 38 | state("temperature", label:'${currentValue}°', unit:"F", 39 | backgroundColors:[ 40 | [value: 31, color: "#153591"], 41 | [value: 44, color: "#1e9cbb"], 42 | [value: 59, color: "#90d2a7"], 43 | [value: 74, color: "#44b621"], 44 | [value: 84, color: "#f1d801"], 45 | [value: 95, color: "#d04e00"], 46 | [value: 96, color: "#bc2323"] 47 | ] 48 | ) 49 | } 50 | valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") { 51 | state "thermostatSetpoint", label:'${currentValue}' 52 | } 53 | controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { 54 | state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb" 55 | } 56 | standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { 57 | state "default", action:"refresh.refresh", icon:"st.secondary.refresh" 58 | } 59 | main "temperature" 60 | details(["temperature", "thermostatSetpoint","coolSliderControl", "refresh"]) 61 | 62 | } 63 | } 64 | 65 | // parse events into attributes 66 | def parse(String description) { 67 | log.debug "Parsing '${description}'" 68 | // TODO: handle 'temperature' attribute 69 | // TODO: handle 'heatingSetpoint' attribute 70 | // TODO: handle 'coolingSetpoint' attribute 71 | // TODO: handle 'thermostatSetpoint' attribute 72 | // TODO: handle 'thermostatMode' attribute 73 | // TODO: handle 'thermostatFanMode' attribute 74 | // TODO: handle 'thermostatOperatingState' attribute 75 | // TODO: handle 'temperature' attribute 76 | 77 | } 78 | 79 | // handle commands 80 | def poll() { 81 | log.debug "Executing 'poll'" 82 | // TODO: handle 'poll' command 83 | } 84 | 85 | def setHeatingSetpoint() { 86 | log.debug "Executing 'setHeatingSetpoint'" 87 | // TODO: handle 'setHeatingSetpoint' command 88 | } 89 | 90 | def setCoolingSetpoint(tempF) { 91 | log.debug "Executing 'setCoolingSetpoint'" 92 | // TODO: handle 'setCoolingSetpoint' command 93 | log.debug tempF 94 | def privMin = 40 95 | def privMax = 90 96 | def PrefSet = false 97 | if (is(SliderMin)) 98 | { 99 | PrefSet = true 100 | } 101 | if (is(SliderMax)) 102 | { 103 | PrefSet = true 104 | } 105 | 106 | // So range is 1-100 need to map to a new min and max formula is y = ((x - a1)/(a2 - a1)) * (b2 - b1) + b1 107 | //OldRange = (OldMax - OldMin) 108 | //NewRange = (NewMax - NewMin) 109 | //NewValue = (((OldValue - OldMin) * NewRange) / OldRange) + NewMin 110 | //def newtempF = ((tempF.toInteger() - 1)/(100 - 1) * (privMax.toInteger() - privMin.toInteger()) + privMax.toInteger()) 111 | def newtempF = (((tempF.toInteger() - 1) * (privMax.toInteger() - privMin.toInteger()) / (100/1) + privMin.toInteger())) 112 | log.debug newtempF 113 | //Set min and max for range 114 | 115 | sendEvent("name":"thermostatSetpoint", "value":newtempF) 116 | } 117 | 118 | def off() { 119 | log.debug "Executing 'off'" 120 | // TODO: handle 'off' command 121 | } 122 | 123 | def heat() { 124 | log.debug "Executing 'heat'" 125 | // TODO: handle 'heat' command 126 | } 127 | 128 | def emergencyHeat() { 129 | log.debug "Executing 'emergencyHeat'" 130 | // TODO: handle 'emergencyHeat' command 131 | } 132 | 133 | def cool() { 134 | log.debug "Executing 'cool'" 135 | // TODO: handle 'cool' command 136 | } 137 | 138 | def setThermostatMode() { 139 | log.debug "Executing 'setThermostatMode'" 140 | // TODO: handle 'setThermostatMode' command 141 | } 142 | 143 | def fanOn() { 144 | log.debug "Executing 'fanOn'" 145 | // TODO: handle 'fanOn' command 146 | } 147 | 148 | def fanAuto() { 149 | log.debug "Executing 'fanAuto'" 150 | // TODO: handle 'fanAuto' command 151 | } 152 | 153 | def fanCirculate() { 154 | log.debug "Executing 'fanCirculate'" 155 | // TODO: handle 'fanCirculate' command 156 | } 157 | 158 | def setThermostatFanMode() { 159 | log.debug "Executing 'setThermostatFanMode'" 160 | // TODO: handle 'setThermostatFanMode' command 161 | } 162 | 163 | def auto() { 164 | log.debug "Executing 'auto'" 165 | // TODO: handle 'auto' command 166 | } 167 | -------------------------------------------------------------------------------- /devicetypes/pstuart/insteon-switch-local.src/insteon-switch-local.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Insteon Switch (LOCAL) 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * Updated 1/4/15 by goldmichael@gmail.com 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at: 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 14 | * for the specific language governing permissions and limitations under the License. 15 | * 16 | */ 17 | metadata { 18 | definition (name: "Insteon Switch (LOCAL)", namespace: "pstuart", author: "patrick@patrickstuart.com") { 19 | capability "Switch" 20 | capability "Sensor" 21 | capability "Actuator" 22 | capability "Water Sensor" 23 | 24 | command "status" 25 | } 26 | 27 | preferences { 28 | input("InsteonIP", "string", title:"Insteon IP Address", description: "Please enter your Insteon Hub IP Address", defaultValue: "192.168.1.2", required: true, displayDuringSetup: true) 29 | input("InsteonPort", "string", title:"Insteon Port", description: "Please enter your Insteon Hub Port", defaultValue: 25105, required: true, displayDuringSetup: true) 30 | input("InsteonID", "string", title:"Device Insteon ID", description: "Please enter the devices Insteon ID - numbers only", defaultValue: "1E65F2", required: true, displayDuringSetup: true) 31 | input("InsteonHubUsername", "string", title:"Insteon Hub Username", description: "Please enter your Insteon Hub Username", defaultValue: "michael" , required: true, displayDuringSetup: true) 32 | input("InsteonHubPassword", "string", title:"Insteon Hub Password", description: "Please enter your Insteon Hub Password", defaultValue: "password" , required: true, displayDuringSetup: true) 33 | } 34 | 35 | simulator { 36 | 37 | } 38 | 39 | // UI tile definitions 40 | tiles { 41 | standardTile("water", "device.water", width: 2, height: 2, canChangeIcon: true) { 42 | state "dry", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 43 | state "wet", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 44 | } 45 | valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { 46 | state "battery", label:'${currentValue} battery', unit:"" 47 | } 48 | standardTile("refresh", "device.button", inactiveLabel: false, decoration: "flat") { 49 | state "refresh", action:"status", icon:"st.secondary.refresh" 50 | } 51 | 52 | main "water" 53 | details(["water", "battery", "refresh"]) 54 | } 55 | } 56 | 57 | // handle commands 58 | def on() { 59 | //log.debug "Executing 'take'" 60 | sendEvent(name: "water", value: "wet") 61 | def host = InsteonIP 62 | 63 | def path = "/3?0262" + "${InsteonID}" + "0F12FF=I=3" 64 | log.debug "path is: $path" 65 | 66 | 67 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}" 68 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 69 | def headers = [:] //"HOST:" 70 | headers.put("HOST", "$host:$InsteonPort") 71 | headers.put("Authorization", userpass) 72 | 73 | 74 | try { 75 | def hubAction = new physicalgraph.device.HubAction( 76 | method: method, 77 | path: path, 78 | headers: headers 79 | ) 80 | } 81 | catch (Exception e) { 82 | log.debug "Hit Exception on $hubAction" 83 | log.debug e 84 | } 85 | } 86 | 87 | def status() { 88 | log.debug "status hit" 89 | def host = InsteonIP 90 | 91 | def path = "/3?0262" + "${InsteonID}" + "0F19FF=I=3" 92 | log.debug "path is: $path" 93 | 94 | 95 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}" 96 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 97 | def headers = [:] //"HOST:" 98 | headers.put("HOST", "$host:$InsteonPort") 99 | headers.put("Authorization", userpass) 100 | 101 | try { 102 | def hubAction = new physicalgraph.device.HubAction( 103 | method: method, 104 | path: path, 105 | headers: headers 106 | ) 107 | } 108 | catch (Exception e) { 109 | log.debug "Hit Exception on $hubAction" 110 | log.debug e 111 | } 112 | 113 | } 114 | 115 | def off() { 116 | //log.debug "Executing 'take'" 117 | sendEvent(name: "water", value: "dry") 118 | def host = InsteonIP 119 | 120 | def path = "/3?0262" + "${InsteonID}" + "0F14FF=I=3" 121 | log.debug "path is: $path" 122 | 123 | 124 | def userpassascii = "${InsteonHubUsername}:${InsteonHubPassword}" 125 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 126 | def headers = [:] //"HOST:" 127 | headers.put("HOST", "$host:$InsteonPort") 128 | headers.put("Authorization", userpass) 129 | 130 | try { 131 | def hubAction = new physicalgraph.device.HubAction( 132 | method: method, 133 | path: path, 134 | headers: headers 135 | ) 136 | } 137 | catch (Exception e) { 138 | log.debug "Hit Exception on $hubAction" 139 | log.debug e 140 | } 141 | } 142 | 143 | def parse(String description) { 144 | log.debug "Parsing $description" 145 | def map = stringToMap(description) 146 | 147 | if (description == "0") 148 | { 149 | sendEvent(name: "water", value: "wet") 150 | } 151 | if (description == "1") 152 | { 153 | sendEvent(name: "water", value: "dry") 154 | } 155 | if (description == "2") 156 | { 157 | sendEvent(name: "battery", value: "low") 158 | } 159 | if (description == "3") 160 | { 161 | sendEvent(name: "battery", value: "ok") 162 | } 163 | } -------------------------------------------------------------------------------- /smartapps/sample-webservice.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample Web Services Application 3 | * 4 | * Author: SmartThings 5 | */ 6 | 7 | preferences { 8 | section("Allow a web application to control these things...") { 9 | input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false 10 | input "motions", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false 11 | input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false 12 | } 13 | } 14 | 15 | mappings { 16 | path("/list") { 17 | action: [ 18 | GET: "listAll" 19 | ] 20 | } 21 | 22 | path("/events/:id") { 23 | action: [ 24 | GET: "showEvents" 25 | ] 26 | } 27 | 28 | path("/switches") { 29 | action: [ 30 | GET: "listSwitches", 31 | PUT: "updateSwitches" 32 | ] 33 | } 34 | path("/switches/:id") { 35 | action: [ 36 | GET: "showSwitch", 37 | PUT: "updateSwitch" 38 | ] 39 | } 40 | path("/switches/subscriptions") { 41 | action: [ 42 | POST: "addSwitchSubscription" 43 | ] 44 | } 45 | path("/switches/subscriptions/:id") { 46 | action: [ 47 | DELETE: "removeSwitchSubscription" 48 | ] 49 | } 50 | 51 | path("/motionSensors") { 52 | action: [ 53 | GET: "listMotions", 54 | PUT: "updateMotions" 55 | ] 56 | } 57 | path("/motionSensors/:id") { 58 | action: [ 59 | GET: "showMotion", 60 | PUT: "updateMotion" 61 | ] 62 | } 63 | path("/motionSensors/subscriptions") { 64 | action: [ 65 | POST: "addMotionSubscription" 66 | ] 67 | } 68 | path("/motionSensors/subscriptions/:id") { 69 | action: [ 70 | DELETE: "removeMotionSubscription" 71 | ] 72 | } 73 | 74 | path("/locks") { 75 | action: [ 76 | GET: "listLocks", 77 | PUT: "updateLock" 78 | ] 79 | } 80 | path("/locks/:id") { 81 | action: [ 82 | GET: "showLock", 83 | PUT: "updateLock" 84 | ] 85 | } 86 | path("/locks/subscriptions") { 87 | action: [ 88 | POST: "addLockSubscription" 89 | ] 90 | } 91 | path("/locks/subscriptions/:id") { 92 | action: [ 93 | DELETE: "removeLockSubscription" 94 | ] 95 | } 96 | 97 | path("/state") { 98 | action: [ 99 | GET: "currentState" 100 | ] 101 | } 102 | 103 | } 104 | 105 | def installed() {log.trace "Installed"} 106 | 107 | def updated() {log.trace "Updated"} 108 | 109 | def listAll() { 110 | listSwitches() + listMotions() + listLocks() 111 | } 112 | 113 | 114 | def listSwitches() { 115 | switches.collect{device(it,"switch")} 116 | } 117 | void updateSwitches() { 118 | updateAll(switches) 119 | } 120 | def showSwitch() { 121 | show(switches, "switch") 122 | } 123 | void updateSwitch() { 124 | update(switches) 125 | } 126 | def addSwitchSubscription() { 127 | addSubscription(switches, "switch") 128 | } 129 | def removeSwitchSubscription() { 130 | removeSubscription(switches) 131 | } 132 | 133 | def listMotions() { 134 | motions.collect{device(it,"motionSensor")} 135 | } 136 | void updateMotions() { 137 | updateAll(motions) 138 | } 139 | def showMotion() { 140 | show(motions, "motionSensor") 141 | } 142 | void updateMotion() { 143 | update(motions) 144 | } 145 | def addMotionSubscription() { 146 | addSubscription(motions, "motion") 147 | } 148 | def removeMotionSubscription() { 149 | removeSubscription(motions) 150 | } 151 | 152 | def listLocks() { 153 | locks.collect{device(it,"lock")} 154 | } 155 | void updateLocks() { 156 | updateAll(locks) 157 | } 158 | def showLock() { 159 | show(locks, "lock") 160 | } 161 | void updateLock() { 162 | update(locks) 163 | } 164 | def addLockSubscription() { 165 | addSubscription(locks, "lock") 166 | } 167 | def removeLockSubscription() { 168 | removeSubscription(locks) 169 | } 170 | 171 | def deviceHandler(evt) { 172 | def deviceInfo = state[evt.deviceId] 173 | if (deviceInfo) { 174 | httpPostJson(uri: deviceInfo.callbackUrl, path: '', body: [evt: [value: evt.value]]) { 175 | log.debug "Event data successfully posted" 176 | } 177 | } else { 178 | log.debug "No subscribed device found" 179 | } 180 | } 181 | 182 | def currentState() { 183 | state 184 | } 185 | 186 | def showStates() { 187 | def device = (switches + motions + locks).find { it.id == params.id } 188 | if (!device) { 189 | httpError(404, "Switch not found") 190 | } 191 | else { 192 | device.events(params) 193 | } 194 | } 195 | 196 | private void updateAll(devices) { 197 | def command = request.JSON?.command 198 | if (command) { 199 | devices."$command"() 200 | } 201 | } 202 | 203 | private void update(devices) { 204 | log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" 205 | def command = request.JSON?.command 206 | if (command) { 207 | def device = devices.find { it.id == params.id } 208 | if (!device) { 209 | httpError(404, "Device not found") 210 | } else { 211 | device."$command"() 212 | } 213 | } 214 | } 215 | 216 | private show(devices, type) { 217 | def device = devices.find { it.id == params.id } 218 | if (!device) { 219 | httpError(404, "Device not found") 220 | } 221 | else { 222 | def attributeName = type == "motionSensor" ? "motion" : type 223 | def s = device.currentState(attributeName) 224 | [id: device.id, label: device.displayName, value: s?.value, unitTime: s?.date?.time, type: type] 225 | } 226 | } 227 | 228 | private addSubscription(devices, attribute) { 229 | def deviceId = request.JSON?.deviceId 230 | def callbackUrl = request.JSON?.callbackUrl 231 | def myDevice = devices.find { it.id == deviceId } 232 | if (myDevice) { 233 | if (state[deviceId]) { 234 | log.debug "Switch subscription already exists, unsubcribing" 235 | unsubscribe(myDevice) 236 | } 237 | log.debug "Adding switch subscription" + callbackUrl 238 | state[deviceId] = [callbackUrl: callbackUrl] 239 | log.debug "Added state: $state" 240 | subscribe(myDevice, "switch", deviceHandler) 241 | } 242 | } 243 | 244 | private removeSubscription(devices) { 245 | def deviceId = params.id 246 | def device = devices.find { it.id == deviceId } 247 | if (device) { 248 | log.debug "Removing $device.displayName subscription" 249 | state.remove(device.id) 250 | unsubscribe(device) 251 | } 252 | } 253 | 254 | private device(it, type) { 255 | it ? [id: it.id, label: it.displayName, type: type] : null 256 | } 257 | -------------------------------------------------------------------------------- /devicetypes/Get Ubi Sensors: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_Ubi 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | 17 | preferences { 18 | input("username", "text", title: "Username", description: "Your Ubi Portal username (usually an email address)") 19 | input("password", "password", title: "Password", description: "Your Ubi password") 20 | } 21 | 22 | metadata { 23 | definition (name: "ps_Ubi", namespace: "ps", author: "patrick@patrickstuart.com") { 24 | capability "Polling" 25 | capability "Relative Humidity Measurement" 26 | capability "Illuminance Measurement" 27 | capability "Temperature Measurement" 28 | 29 | 30 | attribute "airPressure", "string" 31 | attribute "soundLevel", "string" 32 | } 33 | 34 | simulator { 35 | 36 | } 37 | 38 | tiles { 39 | valueTile("temperature", "device.temperature", width: 1, height: 1, canChangeIcon: false) { 40 | state("temperature", label: '${currentValue}°', unit:"F", backgroundColors: [ 41 | [value: 31, color: "#153591"], 42 | [value: 44, color: "#1e9cbb"], 43 | [value: 59, color: "#90d2a7"], 44 | [value: 74, color: "#44b621"], 45 | [value: 84, color: "#f1d801"], 46 | [value: 95, color: "#d04e00"], 47 | [value: 96, color: "#bc2323"] 48 | ] 49 | ) 50 | } 51 | valueTile("humidity", "device.humidity", inactiveLabel: false) { 52 | state "default", label:'${currentValue}%', unit:"Humidity" 53 | } 54 | valueTile("illuminance", "device.illuminance", inactiveLabel: false) { 55 | state "luminosity", label:'${currentValue}lux', unit:"Light" 56 | } 57 | valueTile("airPressure", "device.airPressure", inactiveLabel: false) { 58 | state "default", label:'${currentValue}Kpa', unit:"Air Pressure" 59 | } 60 | valueTile("soundLevel", "device.soundLevel", inactiveLabel: false) { 61 | state "default", label:'${currentValue}db', unit:"Sound" 62 | } 63 | standardTile("refresh", "device.poll", inactiveLabel: false, decoration: "flat") { 64 | state "default", action:"polling.poll", icon:"st.secondary.refresh" 65 | } 66 | 67 | 68 | main "temperature" 69 | details(["temperature", "humidity", "illuminance", "airPressure", "soundLevel", "refresh"]) 70 | } 71 | } 72 | 73 | // parse events into attributes 74 | def parse(String description) { 75 | log.debug "Parsing '${description}'" 76 | // TODO: handle 'humidity' attribute 77 | // TODO: handle 'illuminance' attribute 78 | // TODO: handle 'temperature' attribute 79 | // TODO: handle 'carbonMonoxide' attribute 80 | 81 | } 82 | 83 | // handle commands 84 | def poll() { 85 | log.debug "Executing 'poll'" 86 | 87 | // TODO: handle 'poll' command 88 | login() 89 | } 90 | 91 | 92 | 93 | 94 | def login() { 95 | def params = [ 96 | uri: 'https://portal.theubi.com/login.do', 97 | headers: [ 98 | 'Content-Type': 'application/x-www-form-urlencoded', 99 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 100 | 'Accept-Encoding': 'sdch', 101 | 'Host': 'portal.theubi.com', 102 | 'DNT': '1', 103 | 'Origin': 'https://portal.theubi.com/login.jsp', 104 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36' 105 | ], 106 | body: [j_username: username, j_password: password] 107 | ] 108 | 109 | data.cookies = '' 110 | 111 | httpPost(params) { response -> 112 | log.debug "Request was successful, $response.status" 113 | //log.debug response.headers 114 | response.getHeaders('Set-Cookie').each { 115 | String cookie = it.value.split(';')[0] 116 | //log.debug "Adding cookie to collection: $cookie" 117 | data.cookies = data.cookies + cookie + ';' 118 | } 119 | //log.debug "Cookies: $data.cookies" 120 | 121 | } 122 | 123 | 124 | def params2 = [ 125 | uri: "https://portal.theubi.com/room/", 126 | headers: [ 127 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 128 | 'Accept-Encoding': 'plain', 129 | 'Cookie': data.cookies 130 | ], 131 | ] 132 | 133 | httpGet(params2) { response2 -> 134 | log.debug "Request was successful, $response2.status" 135 | def doc = response2.data 136 | def x = new StringWriter() 137 | doc[0].children[1].children[5].children[1].writeTo(x) 138 | def linesAsList = x.toString().minus(" ").split( /\n|\r|\n\r|\r\n/ ).collect { it.replace( "'", '' ) } 139 | //log.debug linesAsList 140 | def v = [:] 141 | v.temp = linesAsList[6].minus("document.write(toFahrenheit(").minus("));") //temp in C 142 | v.temp = Math.round((Double.parseDouble(v.temp) * 1.8 + 32)*100) /100 // Temp in F 143 | v.humidity = Double.parseDouble(linesAsList[11].minus("Humidity").minus("%")) // humidity 144 | v.airPressure = linesAsList[12].minus("Air Pressure").minus("KPa") //Air Pressure 145 | //v.airPressure = Double.parseDouble(v.airPressure) / 100// into bar 146 | v.soundLevel = Double.parseDouble(linesAsList[13].minus("Sound level").minus("dB")) //Sound Level 147 | v.lightLevel = Double.parseDouble(linesAsList[14].minus("Light level").minus("lux")) //Light Level 148 | log.debug v 149 | 150 | sendEvent(name: 'temperature', value: v.temp) 151 | sendEvent(name: 'humidity', value: v.humidity) 152 | sendEvent(name: 'illuminance', value: v.lightLevel, unit: "lux") 153 | sendEvent(name: 'airPressure', value: v.airPressure.toString()) 154 | sendEvent(name: 'soundLevel', value: v.soundLevel) 155 | 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 SmartThings 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at: 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 11 | * for the specific language governing permissions and limitations under the License. 12 | * 13 | * Laundry Monitor 14 | * 15 | * Author: SmartThings 16 | * 17 | * Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done. 18 | * 19 | * Date: 2013-02-21 20 | */ 21 | 22 | definition( 23 | name: "Laundry Monitor", 24 | namespace: "smartthings", 25 | author: "SmartThings", 26 | description: "Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.", 27 | category: "Convenience", 28 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner.png", 29 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner%402x.png" 30 | ) 31 | 32 | preferences { 33 | section("Tell me when this washer/dryer has stopped..."){ 34 | input "sensor1", "capability.accelerationSensor" 35 | } 36 | section("Via this number (optional, sends push notification if not specified)"){ 37 | input("recipients", "contact", title: "Send notifications to") { 38 | input "phone", "phone", title: "Phone Number", required: false 39 | } 40 | } 41 | section("And by turning on these lights (optional)") { 42 | input "switches", "capability.switch", required: false, multiple: true, title: "Which lights?" 43 | input "lightMode", "enum", options: ["Flash Lights", "Turn On Lights"], required: false, defaultValue: "Turn On Lights", title: "Action?" 44 | } 45 | section("Time thresholds (in minutes, optional)"){ 46 | input "cycleTime", "decimal", title: "Minimum cycle time", required: false, defaultValue: 10 47 | input "fillTime", "decimal", title: "Time to fill tub", required: false, defaultValue: 5 48 | input "cycleCount", "decimal", title: "Number of cycles", required: false, defaultValue : 2 49 | } 50 | } 51 | 52 | def installed() 53 | { 54 | initialize() 55 | } 56 | 57 | def updated() 58 | { 59 | unsubscribe() 60 | log.debug "Monitor has been reset by user in app" 61 | initialize() 62 | } 63 | 64 | def initialize() { 65 | subscribe(sensor1, "acceleration.active", accelerationActiveHandler) 66 | subscribe(sensor1, "acceleration.inactive", accelerationInactiveHandler) 67 | } 68 | 69 | def accelerationActiveHandler(evt) { 70 | log.trace "vibration has been detected" 71 | if (!state.isRunning) { 72 | log.info "Arming detector" 73 | state.isRunning = true 74 | state.startedAt = now() 75 | state.cycleCount = 0 76 | } 77 | state.stoppedAt = null 78 | } 79 | 80 | def accelerationInactiveHandler(evt) { 81 | log.trace "no vibration, isRunning: $state.isRunning" 82 | if (state.isRunning) { 83 | log.debug "startedAt: ${state.startedAt}, stoppedAt: ${state.stoppedAt}" 84 | if (!state.stoppedAt) { 85 | state.stoppedAt = now() 86 | def delay = Math.floor(fillTime * 60).toInteger() 87 | runIn(delay, checkRunning, [overwrite: false]) 88 | log.debug "Waiting $delay and then will check to see if running / vibration" 89 | } 90 | } 91 | } 92 | 93 | def checkRunning() { 94 | log.trace "checkRunning()" 95 | if (state.isRunning) { 96 | def fillTimeMsec = fillTime ? fillTime * 60000 : 300000 97 | def sensorStates = sensor1.statesSince("acceleration", new Date((now() - fillTimeMsec) as Long)) 98 | 99 | if (!sensorStates.find{it.value == "active"}) { 100 | log.debug "Sensor is not active / vibrating" 101 | def cycleTimeMsec = cycleTime ? cycleTime * 60000 : 600000 102 | def duration = now() - state.startedAt 103 | 104 | // test to see if we can loop through cycleCount 105 | // loop through cycleCount 106 | // for each cycle, check if it is still running 107 | // if still running, reset and check again 108 | // else, move to next cycle 109 | // if end of cycleCount, end 110 | if (state.cycleCount.toInteger() < cycleCount.toInteger()) { 111 | 112 | state.cycleCount = state.cycleCount.toInteger() + 1 113 | //might have to delay between each cycle? 114 | 115 | log.debug "Cycle $state.cycleCount is running" 116 | } else 117 | 118 | { 119 | 120 | if (duration - fillTimeMsec > cycleTimeMsec) { 121 | log.debug "Sending notification" 122 | 123 | def msg = "${sensor1.displayName} is finished" 124 | log.info msg 125 | 126 | if (location.contactBookEnabled) { 127 | sendNotificationToContacts(msg, recipients) 128 | } 129 | else { 130 | 131 | if (phone) { 132 | sendSms phone, msg 133 | } else { 134 | sendPush msg 135 | } 136 | 137 | } 138 | 139 | if (switches) { 140 | if (lightMode?.equals("Turn On Lights")) { 141 | switches.on() 142 | } else { 143 | flashLights() 144 | } 145 | } 146 | } else { 147 | log.debug "Not sending notification because machine wasn't running long enough $duration versus $cycleTimeMsec msec" 148 | } 149 | state.isRunning = false 150 | log.info "Disarming detector" 151 | } 152 | } 153 | else { 154 | log.debug "skipping notification because vibration detected again" 155 | } 156 | } 157 | 158 | else { 159 | log.debug "machine no longer running" 160 | } 161 | } 162 | 163 | private flashLights() { 164 | def doFlash = true 165 | def onFor = onFor ?: 1000 166 | def offFor = offFor ?: 1000 167 | def numFlashes = numFlashes ?: 3 168 | 169 | log.debug "LAST ACTIVATED IS: ${state.lastActivated}" 170 | if (state.lastActivated) { 171 | def elapsed = now() - state.lastActivated 172 | def sequenceTime = (numFlashes + 1) * (onFor + offFor) 173 | doFlash = elapsed > sequenceTime 174 | log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}" 175 | } 176 | 177 | if (doFlash) { 178 | log.debug "FLASHING $numFlashes times" 179 | state.lastActivated = now() 180 | log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}" 181 | def initialActionOn = switches.collect{it.currentSwitch != "on"} 182 | def delay = 1L 183 | numFlashes.times { 184 | log.trace "Switch on after $delay msec" 185 | switches.eachWithIndex {s, i -> 186 | if (initialActionOn[i]) { 187 | s.on(delay: delay) 188 | } 189 | else { 190 | s.off(delay:delay) 191 | } 192 | } 193 | delay += onFor 194 | log.trace "Switch off after $delay msec" 195 | switches.eachWithIndex {s, i -> 196 | if (initialActionOn[i]) { 197 | s.off(delay: delay) 198 | } 199 | else { 200 | s.on(delay:delay) 201 | } 202 | } 203 | delay += offFor 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * LIFX Color Bulb 3 | * 4 | * Copyright 2015 LIFX 5 | * 6 | */ 7 | metadata { 8 | definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") { 9 | capability "Actuator" 10 | capability "Color Control" 11 | capability "Color Temperature" 12 | capability "Switch" 13 | capability "Switch Level" // brightness 14 | capability "Polling" 15 | capability "Refresh" 16 | capability "Sensor" 17 | } 18 | 19 | simulator { 20 | // TODO: define status and reply messages here 21 | } 22 | 23 | tiles { 24 | standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { 25 | state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666" 26 | state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" 27 | state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 28 | state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" 29 | state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" 30 | } 31 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { 32 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 33 | } 34 | valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { 35 | state "default", label:'' 36 | } 37 | 38 | controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { 39 | state "color", action:"setColor" 40 | } 41 | 42 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { 43 | state "level", action:"switch level.setLevel" 44 | } 45 | valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") { 46 | state "level", label: '${currentValue}%' 47 | } 48 | 49 | controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") { 50 | state "colorTemp", action:"color temperature.setColorTemperature" 51 | } 52 | valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { 53 | state "colorTemp", label: '${currentValue}K' 54 | } 55 | 56 | main(["switch"]) 57 | details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"]) 58 | } 59 | } 60 | 61 | // parse events into attributes 62 | def parse(String description) { 63 | if (description == 'updated') { 64 | return // don't poll when config settings is being updated as it may time out 65 | } 66 | poll() 67 | } 68 | 69 | // handle commands 70 | def setHue(percentage) { 71 | log.debug "setHue ${percentage}" 72 | parent.logErrors(logObject: log) { 73 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"]) 74 | if (resp.status < 300) { 75 | sendEvent(name: "hue", value: percentage) 76 | sendEvent(name: "switch", value: "on") 77 | } else { 78 | log.error("Bad setHue result: [${resp.status}] ${resp.data}") 79 | } 80 | } 81 | } 82 | 83 | def setSaturation(percentage) { 84 | log.debug "setSaturation ${percentage}" 85 | parent.logErrors(logObject: log) { 86 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"]) 87 | if (resp.status < 300) { 88 | sendEvent(name: "saturation", value: percentage) 89 | sendEvent(name: "switch", value: "on") 90 | } else { 91 | log.error("Bad setSaturation result: [${resp.status}] ${resp.data}") 92 | } 93 | } 94 | } 95 | 96 | def setColor(Map color) { 97 | log.debug "setColor ${color}" 98 | def attrs = [] 99 | def events = [] 100 | color.each { key, value -> 101 | switch (key) { 102 | case "hue": 103 | attrs << "hue:${value * 3.6}" 104 | events << createEvent(name: "hue", value: value) 105 | break 106 | case "saturation": 107 | attrs << "saturation:${value / 100}" 108 | events << createEvent(name: "saturation", value: value) 109 | break 110 | case "colorTemperature": 111 | attrs << "kelvin:${value}" 112 | events << createEvent(name: "colorTemperature", value: value) 113 | break 114 | } 115 | } 116 | parent.logErrors(logObject:log) { 117 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")]) 118 | if (resp.status < 300) { 119 | sendEvent(name: "color", value: color.hex) 120 | sendEvent(name: "switch", value: "on") 121 | events.each { sendEvent(it) } 122 | } else { 123 | log.error("Bad setColor result: [${resp.status}] ${resp.data}") 124 | } 125 | } 126 | } 127 | 128 | def setLevel(percentage) { 129 | log.debug "setLevel ${percentage}" 130 | if (percentage < 1 && percentage > 0) { 131 | percentage = 1 // clamp to 1% 132 | } 133 | if (percentage == 0) { 134 | sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update 135 | return off() // if the brightness is set to 0, just turn it off 136 | } 137 | parent.logErrors(logObject:log) { 138 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"]) 139 | if (resp.status < 300) { 140 | sendEvent(name: "level", value: percentage) 141 | sendEvent(name: "switch", value: "on") 142 | } else { 143 | log.error("Bad setLevel result: [${resp.status}] ${resp.data}") 144 | } 145 | } 146 | } 147 | 148 | def setColorTemperature(kelvin) { 149 | log.debug "Executing 'setColorTemperature' to ${kelvin}" 150 | parent.logErrors() { 151 | def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"]) 152 | if (resp.status < 300) { 153 | sendEvent(name: "colorTemperature", value: kelvin) 154 | sendEvent(name: "color", value: "#ffffff") 155 | sendEvent(name: "saturation", value: 0) 156 | } else { 157 | log.error("Bad setLevel result: [${resp.status}] ${resp.data}") 158 | } 159 | 160 | } 161 | } 162 | 163 | def on() { 164 | log.debug "Device setOn" 165 | parent.logErrors() { 166 | if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) { 167 | sendEvent(name: "switch", value: "on") 168 | } 169 | } 170 | } 171 | 172 | def off() { 173 | log.debug "Device setOff" 174 | parent.logErrors() { 175 | if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) { 176 | sendEvent(name: "switch", value: "off") 177 | } 178 | } 179 | } 180 | 181 | def poll() { 182 | log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" 183 | def resp = parent.apiGET("/lights/${device.deviceNetworkId}") 184 | if (resp.status != 200) { 185 | log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") 186 | return [] 187 | } 188 | def data = resp.data 189 | 190 | sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100)) 191 | sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") 192 | sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int)) 193 | sendEvent(name: "hue", value: data.color.hue / 3.6) 194 | sendEvent(name: "saturation", value: data.color.saturation * 100) 195 | sendEvent(name: "colorTemperature", value: data.color.kelvin) 196 | 197 | return [] 198 | } 199 | 200 | def refresh() { 201 | log.debug "Executing 'refresh'" 202 | poll() 203 | } 204 | -------------------------------------------------------------------------------- /devicetypes/pstuart/generic-camera.src/generic-camera.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Generic Camera Device v1.1.01302017 4 | * 5 | * Copyright 2017 patrick@patrickstuart.com 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at: 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 14 | * for the specific language governing permissions and limitations under the License. 15 | * 16 | */ 17 | 18 | metadata { 19 | definition (name: "Generic Camera", namespace: "pstuart", author: "patrick@patrickstuart.com") { 20 | capability "Image Capture" 21 | capability "Sensor" 22 | capability "Actuator" 23 | 24 | attribute "hubactionMode", "string" 25 | 26 | } 27 | 28 | preferences { 29 | input("CameraIP", "string", title:"Camera IP Address", description: "Please enter your camera's IP Address", required: true, displayDuringSetup: true) 30 | input("CameraPort", "string", title:"Camera Port", description: "Please enter your camera's Port", defaultValue: 80 , required: true, displayDuringSetup: true) 31 | input("CameraPath", "string", title:"Camera Path to Image", description: "Please enter the path to the image", defaultValue: "/SnapshotJPEG?Resolution=640x480&Quality=Clarity", required: true, displayDuringSetup: true) 32 | input("CameraAuth", "bool", title:"Does Camera require User Auth?", description: "Please choose if the camera requires authentication (only basic is supported)", defaultValue: true, displayDuringSetup: true) 33 | input("CameraPostGet", "string", title:"Does Camera use a Post or Get, normally Get?", description: "Please choose if the camera uses a POST or a GET command to retreive the image", defaultValue: "GET", displayDuringSetup: true) 34 | input("CameraUser", "string", title:"Camera User", description: "Please enter your camera's username", required: false, displayDuringSetup: true) 35 | input("CameraPassword", "string", title:"Camera Password", description: "Please enter your camera's password", required: false, displayDuringSetup: true) 36 | } 37 | 38 | simulator { 39 | 40 | } 41 | /* 42 | tiles { 43 | standardTile("camera", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: true) { 44 | state "default", label: "", action: "", icon: "st.camera.dropcam-centered", backgroundColor: "#FFFFFF" 45 | } 46 | 47 | carouselTile("cameraDetails", "device.image", width: 3, height: 2) { } 48 | 49 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { 50 | state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 51 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0" 52 | //state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 53 | } 54 | standardTile("blank", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 55 | state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF" 56 | } 57 | main "camera" 58 | details(["cameraDetails", "blank", "take"]) 59 | } 60 | 61 | */ 62 | tiles { 63 | standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { 64 | state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 65 | state "taking", label:'Taking', action: "", icon: "st.camera.take-photo", backgroundColor: "#53a7c0" 66 | state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.camera", backgroundColor: "#FFFFFF", nextState:"taking" 67 | } 68 | 69 | standardTile("refresh", "device.alarmStatus", inactiveLabel: false, decoration: "flat") { 70 | state "refresh", action:"polling.poll", icon:"st.secondary.refresh" 71 | } 72 | standardTile("blank", "device.image", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false, decoration: "flat") { 73 | state "blank", label: "", action: "", icon: "", backgroundColor: "#FFFFFF" 74 | } 75 | carouselTile("cameraDetails", "device.image", width: 3, height: 2) { } 76 | main "take" 77 | details([ "take", "blank", "refresh", "cameraDetails"]) 78 | } 79 | } 80 | 81 | def parse(String description) { 82 | log.debug "Parsing '${description}'" 83 | def map = [:] 84 | def retResult = [] 85 | def descMap = parseDescriptionAsMap(description) 86 | //Image 87 | def imageKey = descMap["tempImageKey"] ? descMap["tempImageKey"] : descMap["key"] 88 | if (imageKey) { 89 | storeTemporaryImage(imageKey, getPictureName()) 90 | } 91 | } 92 | 93 | // handle commands 94 | def take() { 95 | def userpassascii = "${CameraUser}:${CameraPassword}" 96 | def userpass = "Basic " + userpassascii.encodeAsBase64().toString() 97 | def host = CameraIP 98 | def hosthex = convertIPtoHex(host).toUpperCase() //thanks to @foxxyben for catching this 99 | def porthex = convertPortToHex(CameraPort).toUpperCase() 100 | device.deviceNetworkId = "$hosthex:$porthex" 101 | 102 | log.debug "The device id configured is: $device.deviceNetworkId" 103 | 104 | def path = CameraPath 105 | log.debug "path is: $path" 106 | log.debug "Requires Auth: $CameraAuth" 107 | log.debug "Uses which method: $CameraPostGet" 108 | 109 | def headers = [:] 110 | headers.put("HOST", "$host:$CameraPort") 111 | if (CameraAuth) { 112 | headers.put("Authorization", userpass) 113 | } 114 | 115 | log.debug "The Header is $headers" 116 | 117 | def method = "GET" 118 | try { 119 | if (CameraPostGet.toUpperCase() == "POST") { 120 | method = "POST" 121 | } 122 | } 123 | catch (Exception e) { // HACK to get around default values not setting in devices 124 | settings.CameraPostGet = "GET" 125 | log.debug e 126 | log.debug "You must not of set the perference for the CameraPOSTGET option" 127 | } 128 | 129 | log.debug "The method is $method" 130 | 131 | try { 132 | def hubAction = new physicalgraph.device.HubAction( 133 | method: method, 134 | path: path, 135 | headers: headers 136 | ) 137 | 138 | hubAction.options = [outputMsgToS3:true] 139 | log.debug hubAction 140 | hubAction 141 | } 142 | catch (Exception e) { 143 | log.debug "Hit Exception $e on $hubAction" 144 | } 145 | 146 | } 147 | 148 | def parseDescriptionAsMap(description) { 149 | description.split(",").inject([:]) { map, param -> 150 | def nameAndValue = param.split(":") 151 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 152 | } 153 | } 154 | 155 | private getPictureName() { 156 | def pictureUuid = java.util.UUID.randomUUID().toString().replaceAll('-', '') 157 | log.debug pictureUuid 158 | def picName = device.deviceNetworkId.replaceAll(':', '') + "_$pictureUuid" + ".jpg" 159 | return picName 160 | } 161 | 162 | private String convertIPtoHex(ipAddress) { 163 | String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join() 164 | log.debug "IP address entered is $ipAddress and the converted hex code is $hex" 165 | return hex 166 | 167 | } 168 | 169 | private String convertPortToHex(port) { 170 | String hexport = port.toString().format( '%04x', port.toInteger() ) 171 | log.debug hexport 172 | return hexport 173 | } 174 | 175 | private Integer convertHexToInt(hex) { 176 | Integer.parseInt(hex,16) 177 | } 178 | 179 | 180 | private String convertHexToIP(hex) { 181 | log.debug("Convert hex to ip: $hex") 182 | [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") 183 | } 184 | 185 | private getHostAddress() { 186 | def parts = device.deviceNetworkId.split(":") 187 | log.debug device.deviceNetworkId 188 | def ip = convertHexToIP(parts[0]) 189 | def port = convertHexToInt(parts[1]) 190 | return ip + ":" + port 191 | } 192 | -------------------------------------------------------------------------------- /devicetypes/ps/ps-cardaccess-zigbee-to-ir.src/ps-cardaccess-zigbee-to-ir.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_CardAccess Zigbee to IR 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "ps_CardAccess Zigbee to IR", namespace: "ps", author: "patrick@patrickstuart.com") { 18 | capability "Contact Sensor" 19 | capability "Polling" 20 | capability "Signal Strength" 21 | capability "Sensor" 22 | capability "Refresh" 23 | capability "Configuration" 24 | capability "Button" 25 | 26 | command "test" 27 | command "getClusters" 28 | 29 | fingerprint endpointId: "01", deviceId: "0101", profileId: "0104", inClusters: "0000, 000A, 0099" //01 0104 0101 00 03 0000 000A 0099 00 30 | //fingerprint endpointId: "01", profileId: "0104", deviceId: "0101", inClusters: "0000 0003 0004 0005 0006 0008 000A" 31 | //fingerprint inClusters: "000A", endpointId: "01", deviceId: "0101", profileId: "0104", deviceVersion: "00" 32 | //fingerprint inClusters: "0408", outClusters: "AD0A", deviceVersion: "02", profileId: "C25D", endpointId: "91", deviceId: "0001" 33 | //fingerprint endpointId: "0C", profileId: "0104", deviceId: "0101", deviceVersion: "00", inClusters: "0000, 000A, 0099" 34 | //01 0104 0101 00 03 0000 000A 0099 00 35 | } 36 | 37 | simulator { 38 | // TODO: define status and reply messages here 39 | } 40 | 41 | tiles { 42 | standardTile("SendIR", "device.button") { 43 | state "SendIR", action:"test2", label:'${name}', icon:"st.unknown.zwave.remote-controller", backgroundColor: '#e14902' 44 | } 45 | standardTile("Test", "device.button") { 46 | state "Test", label:'${name}', action:"test", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902' 47 | } 48 | standardTile("Test2", "device.button") { 49 | state "Test2", label:'${name}', action:"test2", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902' 50 | } 51 | standardTile("refresh", "device.alarmStatus", inactiveLabel: false, decoration: "flat") { 52 | state "refresh", action:"refresh", icon:"st.secondary.refresh" 53 | } 54 | 55 | main(["SendIR"]) 56 | details(["SendIR", "Test", "Test2","refresh"]) 57 | } 58 | } 59 | 60 | // parse events into attributes 61 | def parse(String description) { 62 | log.debug "Parsing '${description}'" 63 | if (description?.startsWith("catchall:")) { 64 | def msg = zigbee.parse(description) 65 | log.trace msg 66 | //log.trace "data: $msg.data" 67 | //log.debug msg 68 | def payloadhex = description.tokenize(" ").last() 69 | def payload = payloadhex.decodeHex() 70 | def x = "" 71 | 72 | payload.each() { x += it as char } 73 | 74 | log.debug "Payload is $x" 75 | 76 | 77 | } 78 | 79 | 80 | } 81 | 82 | 83 | // handle commands 84 | def poll() { 85 | log.debug "Executing 'poll'" 86 | // TODO: handle 'poll' command 87 | } 88 | 89 | def refresh() { 90 | log.debug "Refresh hit" 91 | /* 92 | //zigbee.refreshData("0", "4") + zigbee.refreshData("0", "5") 93 | 94 | //def payload = "0s1234 c4.wc0 q01\r\n".encodeAsHex() 95 | def payload = "0s1234 c4.wc0 q01\r\n".encodeAsHex() 96 | log.debug payload 97 | log.debug swapEndianHex(payload) 98 | //"send 0x${device.deviceNetworkId} 1 C25C {${swapEndianHex(payload)}}" 99 | //"st cmd 0x${device.deviceNetworkId} ${endpointId} C25C 1 {${swapEndianHex(payload)}}" 100 | //def cmd = "send 0x${device.deviceNetworkId} C5 C5 {${swapEndianHex(payload)}}" 101 | def cmd = [ 102 | //"zdo bind 0x${device.deviceNetworkId} 0C 01 0099 {${device.zigbeeId}} {}", 103 | //"zdo bind 0x${device.deviceNetworkId} 0x01 0x0c 0x0101 {${device.zigbeeId}} {}", 104 | //"delay 500", 105 | //"zcl global send-me-a-report 0104 0C 0099 0 67 {71}", 106 | //"delay 500", 107 | "st send 0x${device.deviceNetworkId} 0C 01 0C {6771}" 108 | ] 109 | //def cmd = "st cmd 0x${device.deviceNetworkId} 0x0C 0x0099 0x67 {0x71}" 110 | //def cmd = "zcl global send-me-a-report 0104 0C 0099 0 67 {71}" 111 | log.debug cmd 112 | cmd 113 | /* 114 | [ 115 | "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 500", 116 | "st rattr 0x${device.deviceNetworkId} 1 0101 0", "delay 500", 117 | "st rattr 0x${device.deviceNetworkId} 1 C25C 0xC5", "delay 500", 118 | "st rattr 0x${device.deviceNetworkId} 1 C25C 0x0c40", "delay 500" 119 | ] 120 | */ 121 | 122 | } 123 | 124 | def configure() { 125 | /*String zigbeeId = swapEndianHex(device.hub.zigbeeId) 126 | log.debug "Confuguring Reporting and Bindings." 127 | def configCmds = [ 128 | "zdo bind 0x${device.deviceNetworkId} 0x01 0x0c 0x0101 {${device.zigbeeId}} {}", 129 | "delay 500", 130 | "zcl global send-me-a-report 0104 0C 0099 0 67 {71}", 131 | "delay 500" 132 | //Lock Reporting 133 | //"zcl global send-me-a-report 0104 0 0x30 0 3600 {01}", "delay 500", 134 | //"send 0x${device.deviceNetworkId} 1 1", "delay 1000", 135 | 136 | //Battery Reporting 137 | //"zcl global send-me-a-report 1 0x20 0x20 5 3600 {}", "delay 200", 138 | //"send 0x${device.deviceNetworkId} 1 1", "delay 1500", 139 | 140 | 141 | //"zdo bind 0x${device.deviceNetworkId} 1 1 0101 {${device.zigbeeId}} {}", "delay 500", 142 | //"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}" 143 | 144 | ] 145 | return configCmds + refresh() 146 | */ 147 | } 148 | 149 | def test() { 150 | log.debug "test hit" 151 | //"st rattr 0x${device.deviceNetworkId} 0xc5 0c001 0" 152 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 67 {71}" 153 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 67 {73 6c 00 00 ff 10 10 80}" //turn led on to green for 120 sec 154 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 01 01}" //send ir command on channel 1 155 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 02 01}" //unknown 156 | //"st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 01 02}" //double ir duration 157 | //0x74 == 't' for transmit on port 1 158 | //0x04 == length of payload to follow 159 | //0x00 is 'code slot' of cached ir code 160 | //0x00 is the ir format (normal=0, no_carrier=1, inverted=2, direct=3) 161 | // last two bytes are repeat count with LSByte first "little-endian" 162 | 163 | "st cmd 0x${device.deviceNetworkId} 1 0x99 73 {74 04 00 00 06 00}" 164 | /* 165 | response of type 0x74 contains current status. 166 | Following byte (0xc4) is a status = bitmask of DELAYING=0x80, COMPLETED=0x08, TRANSMITTING=0x04, CONTINUOUS=0x02, IDLE=0x00 167 | So, the first response of 0x74, 0xC4... means transmitting, delaying (timed transmit) 168 | the second response of 0x74 0x08 means transmit COMPLETED 169 | The extra byte that follow the TRANSMITTING status are the CRC of the cached IR payload in the Z2IR. 170 | The driver can use this to verify that what the Z2IR is transmitting matches what the driver *thinks* is being transmitted. 171 | 172 | */ 173 | } 174 | 175 | def test2() { 176 | log.debug "Hit test 2" 177 | "st rattr 0x${device.deviceNetworkId} 0xc5 0 0" 178 | } 179 | 180 | def getClusters() { 181 | log.debug "getClusters hit $device.deviceNetworkId" 182 | //"st rattr 0x${device.deviceNetworkId} 4 6 0x01" 183 | "zdo active 0x${device.deviceNetworkId}" 184 | } 185 | 186 | private hex(value, width=2) { 187 | def s = new BigInteger(Math.round(value).toString()).toString(16) 188 | while (s.size() < width) { 189 | s = "0" + s 190 | } 191 | s 192 | } 193 | 194 | private Integer convertHexToInt(hex) { 195 | Integer.parseInt(hex,16) 196 | } 197 | 198 | private String swapEndianHex(String hex) { 199 | reverseArray(hex.decodeHex()).encodeHex() 200 | } 201 | 202 | private byte[] reverseArray(byte[] array) { 203 | int i = 0; 204 | int j = array.length - 1; 205 | byte tmp; 206 | while (j > i) { 207 | tmp = array[j]; 208 | array[j] = array[i]; 209 | array[i] = tmp; 210 | j--; 211 | i++; 212 | } 213 | return array 214 | } -------------------------------------------------------------------------------- /devicetypes/Control4 Zigbee HA Dimmer.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_Control4_Dimmer_ZigbeeHA 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | metadata { 17 | definition (name: "ps_Control4_Dimmer_ZigbeeHA", namespace: "ps", author: "patrick@patrickstuart.com") { 18 | capability "Switch Level" 19 | capability "Actuator" 20 | capability "Switch" 21 | capability "Configuration" 22 | capability "Refresh" 23 | capability "Polling" 24 | 25 | fingerprint endpointId: "01", profileId: "0104", deviceId: "0101", inClusters: "0000 0003 0004 0005 0006 0008 000A" 26 | fingerprint endpointId: "C4", profileId: "C25D", deviceId: "0101", inClusters: "0001" 27 | 28 | } 29 | 30 | preferences { 31 | input("OnSpeed", "text", title:"Turn On Speed", description: "Please enter the speed at which the dimmer turns on", defaultValue:"1500", required: true, displayDuringSetup: true) 32 | input("OffSpeed", "text", title:"Turn Off Speed", description: "Please enter the speed at which the dimmer turns off", defaultValue:"1500", required: true, displayDuringSetup: true) 33 | input("DefaultOnValue", "text", title:"Default On Value", description: "Please enter the default value you want ST to turn on to, in case the last dimmed value is lost.", defaultValue:"75", required: true, displayDuringSetup: true) 34 | 35 | } 36 | simulator { 37 | // status messages 38 | status "on": "on/off: 1" 39 | status "off": "on/off: 0" 40 | 41 | // reply messages 42 | reply "zcl on-off on": "on/off: 1" 43 | reply "zcl on-off off": "on/off: 0" 44 | 45 | command "test" 46 | command "getClusters" 47 | } 48 | 49 | tiles { 50 | standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { 51 | state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" 52 | state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" 53 | } 54 | standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { 55 | state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" 56 | } 57 | controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { 58 | state "level", action:"switch level.setLevel" 59 | } 60 | valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { 61 | state "level", label: 'Level ${currentValue}%' 62 | } 63 | 64 | main(["switch"]) 65 | details(["switch", "levelSliderControl", "level", "refresh"]) 66 | } 67 | } 68 | 69 | def parse(String description) { 70 | //log.trace description 71 | if (description?.startsWith("catchall: C25")) { 72 | def msg = zigbee.parse(description) 73 | //log.trace msg 74 | def payloadhex = description.tokenize(" ").last() 75 | def payload = payloadhex.decodeHex() 76 | def x = "" 77 | payload.each() { x += it as char } 78 | 79 | //log.debug "Payload is $x" 80 | 81 | if(x.contains("sa c4.dm.cc 00 01")) 82 | { 83 | def result = createEvent(name: "switch", value: "on") 84 | log.debug "Parse returned ${result?.descriptionText}" 85 | return result 86 | } 87 | if(x.contains("sa c4.dm.cc 00 00")) 88 | { 89 | def result = createEvent(name: "switch", value: "off") 90 | log.debug "Parse returned ${result?.descriptionText}" 91 | return result 92 | } 93 | if(x.contains("sa c4.dm.cc 00 02")) 94 | { 95 | log.debug "Double Tap Top" 96 | } 97 | if(x.contains("sa c4.dm.cc 01 02")) 98 | { 99 | log.debug "Double Tap Bottom" 100 | } 101 | if(x.contains("sa c4.dm.cc 00 03")) 102 | { 103 | log.debug "Triple Tap Top" 104 | } 105 | if(x.contains("sa c4.dm.cc 01 03")) 106 | { 107 | log.debug "Triple Tap Bottom" 108 | } 109 | if(x.contains("sa c4.dm.t0c") || x.contains("sa c4.dm.b0c")) { 110 | log.debug "switch is dimming $x" 111 | def l = convertHexToInt(x.tokenize(" ").last().split()) 112 | log.debug l 113 | def i = Math.round(l) 114 | sendEvent( name: "level", value: i ) 115 | } 116 | 117 | } 118 | 119 | if (description?.startsWith("read attr")) { 120 | //log.debug "Read Attr found" 121 | //def descMap = parseDescriptionAsMap(description) 122 | //log.debug descMap 123 | //log.debug description[-2..-1] 124 | def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) 125 | 126 | sendEvent( name: "level", value: i ) 127 | } 128 | } 129 | 130 | def test() { 131 | /* 132 | def cmd = [] 133 | cmd << "zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}" 134 | cmd << "delay 500" 135 | cmd << "send 0x${device.deviceNetworkId} 1 6" 136 | cmd << "delay 1000" 137 | cmd 138 | */ 139 | //'zcl on-off on' 140 | log.debug "$OnSpeed onspeed $OffSpeed offspeed $DefaultOnValue defaultOnValue $state.lastOnValue is state.lastonvalue" 141 | } 142 | 143 | def getClusters() { 144 | log.debug "getClusters hit $device.deviceNetworkId" 145 | //"st rattr 0x${device.deviceNetworkId} 4 6 0x01" 146 | "zdo active 0x${device.deviceNetworkId}" 147 | } 148 | 149 | def on() { 150 | log.debug "on()" 151 | sendEvent(name: "switch", value: "on") 152 | 153 | //"st cmd 0x${device.deviceNetworkId} 1 6 1 {}" 154 | 155 | //get level in UI 156 | def value = device.currentValue("level") 157 | if (value == 0) { value = state.lastOnValue 158 | log.debug "Value is $value" 159 | } 160 | def level = new BigInteger(Math.round(value * 255 / 100).toString()).toString(16) 161 | //log.debug level 162 | 163 | def speed = OnSpeed //.toString().padLeft(4, '0') 164 | log.debug speed 165 | "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${speed} }" 166 | } 167 | 168 | def off() { 169 | log.debug "off()" 170 | //state.lastOnValue = device.currentValue("level") 171 | sendEvent(name: "switch", value: "off") 172 | //"st cmd 0x${device.deviceNetworkId} 1 6 0 {}" 173 | def speed = OffSpeed.toString().padLeft(4, '0') 174 | log.debug speed 175 | "st cmd 0x${device.deviceNetworkId} 1 8 4 {00 ${speed} }" 176 | } 177 | 178 | def refresh() { 179 | [ 180 | "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 100", 181 | "st rattr 0x${device.deviceNetworkId} 1 8 0" 182 | ] 183 | } 184 | 185 | def poll(){ 186 | log.debug "Poll is calling refresh" 187 | refresh() 188 | } 189 | 190 | def setLevel(value) { setLevel(value,"0500") } 191 | 192 | def setLevel(value, speed) { 193 | log.debug value 194 | log.debug speed //.toString().padLeft(4, '0') 195 | state.lastOnValue = value 196 | speed = speed.toString().padLeft(4, '0') 197 | log.trace "setLevel($value)" 198 | 199 | def cmds = [] 200 | if (value < 8.0) { 201 | log.debug "Value equals 0?" 202 | sendEvent(name: "switch", value: "off") 203 | 204 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {00 0500}" 205 | //cmds << "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" 206 | } 207 | else if (device.latestValue("switch") == "off") { 208 | sendEvent(name: "switch", value: "on") 209 | } 210 | 211 | sendEvent(name: "level", value: value) 212 | def level = new BigInteger(Math.round(value * 255 / 100).toString()).toString(16) 213 | cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${speed} }" 214 | cmds 215 | } 216 | 217 | //def setLevel(value) { 218 | 219 | //} 220 | 221 | 222 | def configure() { 223 | 224 | String zigbeeId = swapEndianHex(device.hub.zigbeeId) 225 | log.debug "Confuguring Reporting and Bindings." 226 | def configCmds = [ 227 | 228 | //Switch Reporting 229 | "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", 230 | "send 0x${device.deviceNetworkId} 1 1", "delay 1000", 231 | 232 | //Level Control Reporting 233 | "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", 234 | "send 0x${device.deviceNetworkId} 1 1", "delay 1500", 235 | 236 | "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1500", 237 | "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", 238 | ] 239 | return configCmds + refresh() // send refresh cmds as part of config 240 | } 241 | 242 | def uninstalled() { 243 | 244 | log.debug "uninstalled()" 245 | response("zcl rftd") 246 | 247 | } 248 | 249 | private getEndpointId() { 250 | //log.debug "Device.endpoint is $device.endpointId" 251 | new BigInteger(device.endpointId, 16).toString() 252 | } 253 | 254 | private hex(value, width=2) { 255 | def s = new BigInteger(Math.round(value).toString()).toString(16) 256 | while (s.size() < width) { 257 | s = "0" + s 258 | } 259 | s 260 | } 261 | 262 | private Integer convertHexToInt(hex) { 263 | Integer.parseInt(hex,16) 264 | } 265 | 266 | 267 | private String swapEndianHex(String hex) { 268 | reverseArray(hex.decodeHex()).encodeHex() 269 | } 270 | 271 | private byte[] reverseArray(byte[] array) { 272 | int i = 0; 273 | int j = array.length - 1; 274 | byte tmp; 275 | while (j > i) { 276 | tmp = array[j]; 277 | array[j] = array[i]; 278 | array[i] = tmp; 279 | j--; 280 | i++; 281 | } 282 | return array 283 | } 284 | 285 | def parseDescriptionAsMap(description) { 286 | (description - "read attr - ").split(",").inject([:]) { map, param -> 287 | def nameAndValue = param.split(":") 288 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /smartapps/pstuart/live-code-friday-virtual-device-manager.src/live-code-friday-virtual-device-manager.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Live Code Friday Virtual Device Manager 3 | * 4 | * Copyright 2015 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * Tasks 16 | Create a dynamic pages interface 17 | Pick a device type 18 | add/delete that child 19 | 20 | */ 21 | definition( 22 | name: "Live Code Friday Virtual Device Manager", 23 | namespace: "pstuart", 24 | author: "Patrick Stuart", 25 | description: "Live Code Friday Virtual Device Manager", 26 | category: "My Apps", 27 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 28 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 29 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 30 | singleInstance: true) 31 | 32 | 33 | preferences { 34 | page(name: "firstPage") 35 | page(name: "inputPage") 36 | page(name: "devicePage") 37 | page(name: "addDevicePage") 38 | page(name: "viewDevicePage") 39 | page(name: "deletePage") 40 | } 41 | 42 | def firstPage() { 43 | dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) { 44 | section("Main Menu") { 45 | paragraph "Version 1.2" 46 | href(page: "inputPage", title: "Let's Add Devices!") 47 | 48 | } 49 | /* 50 | section("Later") { 51 | paragraph "More to come..." 52 | } 53 | */ 54 | 55 | } 56 | } 57 | 58 | def inputPage() { 59 | dynamicPage(name: "inputPage", title: "Choose What Device Type You Want To Add", nextPage:"firstPage") { 60 | section("Device Types") { 61 | href(page: "devicePage", title: "Switches", params: [device: "Generic Switch"]) 62 | href(page: "devicePage", title: "Dimmers", params: [device: "Generic Dimmer"]) 63 | href(page: "devicePage", title: "Contact Sensor", params: [device: "Generic Contact"]) 64 | 65 | } 66 | section("Later") { 67 | paragraph "more devices coming soon" 68 | } 69 | section("Navigation") { 70 | href(page: "firstPage", title: "Main Menu") 71 | } 72 | } 73 | } 74 | 75 | def devicePage(params) { 76 | dynamicPage(name: "devicePage", title: "Devices", nextPage:"inputPage") { 77 | // Loop childDevices based on type 78 | // match up types 79 | def device = params.device 80 | log.debug "Hit Device Page with the selector device type $device" 81 | def deviceTitle = device + "s" 82 | if (device?.endsWith('ch') || device?.endsWith('s') || device?.endsWith('x')) { 83 | deviceTitle = device + "es" 84 | } 85 | log.debug "Device Title is $deviceTitle" 86 | 87 | section("Installed ${deviceTitle}") { 88 | def childDevices = getChildDevices() 89 | 90 | log.debug "The Child Devices are $childDevices" 91 | if (childDevices) { 92 | def devices = "Switches Installed:\r\nThis is a second line\r\n" 93 | log.debug "Inside childDevices if statement" 94 | 95 | childDevices.findAll { it.typeName == device } 96 | .each { 97 | log.debug "The child device id is $it.deviceNetworkId and the type is $it" 98 | def test = it.typeName 99 | log.debug "Testing $test" 100 | //def tempDevice = getChildDevice(it) 101 | //log.debug tempDevice 102 | //devices = devices + "test\r\n" 103 | 104 | 105 | 106 | switch(it.typeName) { 107 | case "Generic Switch" : 108 | href(page: "viewDevicePage", title: it.name, params: [dni: it.deviceNetworkId]) 109 | break 110 | case "Generic Dimmer" : 111 | href(page: "viewDevicePage", title: it.name, params: [dni: it.deviceNetworkId]) 112 | break 113 | case "Generic Contact" : 114 | href(page: "viewDevicePage", title: it.name, params: [dni: it.deviceNetworkId]) 115 | break 116 | default : break 117 | 118 | 119 | } 120 | 121 | } 122 | } else { 123 | paragraph "No Virtual Generic Devices are Installed" 124 | } 125 | } 126 | 127 | section("Add A ${params.device}") { //${params.device} 128 | // List Switches getChildDevices() 129 | 130 | // Add A Switch addChildDevice() 131 | // View A Switch / Delete that switch go to switch view 132 | input("DeviceName", "text") 133 | href(page: "addDevicePage", title: "New $device", params: [type: device]) 134 | } 135 | 136 | section("Navigation") { 137 | href(page: "firstPage", title: "Main Menu") 138 | } 139 | } 140 | } 141 | 142 | def addDevicePage(params) { 143 | dynamicPage(name: "addDevicePage", title: "New $params.type", nextPage:"devicePage") { 144 | section("New $params.type Add Result") { 145 | //add new virtual switch 146 | log.debug "Add Device Page hit with params $params and $settings.DeviceName" 147 | def newDeviceName = params.type 148 | if (settings.DeviceName) { 149 | newDeviceName = settings.DeviceName 150 | } 151 | def result = addChildDevice(params.type, newDeviceName) 152 | paragraph "$params.type Added ${result}" 153 | 154 | href(page: "devicePage", title: "Devices") 155 | } 156 | 157 | section("Navigation") { 158 | href(page: "firstPage", title: "Main Menu") 159 | } 160 | } 161 | } 162 | 163 | def viewDevicePage(params) { 164 | dynamicPage(name: "viewDevicePage", title: "Switch", nextPage:"devicePage") { 165 | def viewSwitch = getChildDevice(params.dni) 166 | section("$viewSwitch.name") { 167 | paragraph "Switch Details \r\nName: $viewSwitch.name\r\nType: $viewSwitch.typeName\r\nNetwork ID: $viewSwitch.deviceNetworkId\r\nStates\r\nSwitch: ${viewSwitch.currentState('switch').value}\r\n" // Create info about switch / child device 168 | log.debug viewSwitch.currentState('switch').value 169 | href(page: "deletePage", title: "Delete", params: [dni: params.dni]) 170 | } 171 | 172 | section("Navigation") { 173 | href(page: "firstPage", title: "Main Menu") 174 | } 175 | } 176 | } 177 | 178 | def deletePage(params) { 179 | dynamicPage(name: "deletePage", title: "Delete", nextPage:"devicePage") { 180 | section("switch") { 181 | paragraph "Deleted Switch with DNI of $params.dni" 182 | log.debug "Deleting $params.dni" 183 | //def delete = getChildDevices().findAll { it?.contains(params.dni) } 184 | //log.debug delete 185 | def delete = getChildDevice(params.dni) 186 | //removeChildDevices(delete) 187 | deleteChildDevice(delete.deviceNetworkId) 188 | 189 | href(page: "switchPage", title: "Switches") 190 | } 191 | 192 | section("Navigation") { 193 | href(page: "firstPage", title: "Main Menu") 194 | } 195 | } 196 | } 197 | 198 | def installed() { 199 | log.debug "Installed with settings: ${settings}" 200 | 201 | initialize() 202 | } 203 | 204 | def updated() { 205 | log.debug "Updated with settings: ${settings}" 206 | 207 | unsubscribe() 208 | initialize() 209 | } 210 | 211 | def initialize() { 212 | // TODO: subscribe to attributes, devices, locations, etc. 213 | def switches = getChildDevices() 214 | switches.each { 215 | if (!it.currentValue('switch') ) { 216 | it.off() 217 | } 218 | } 219 | } 220 | 221 | 222 | def addChildDevice(params, deviceName) { 223 | //Get all devices installed as children 224 | def childDevices = getChildDevices() //.findAll{ it -> it.type == params } //Find device of type params.type 225 | //def collectDevices = childDevices.collect{ getChildDevice(it).type ?: 0} 226 | //log.debug "The result of collectDevices is $collectDevices" 227 | 228 | def gTypes = genericTypes() 229 | log.debug gTypes 230 | def subChildDevices = childDevices?.findAll { it -> it.typeName == params } 231 | log.debug "The subset of child devices is $subChildDevices based on type $params" 232 | def counter = subChildDevices?.size() + 1 233 | log.debug "$subChildDevices and counter is $counter" 234 | 235 | /* 236 | def counters = [:] 237 | childDevices.each { 238 | def childDevice = getChildDevice(it) 239 | log.debug "Child Device type is $childDevice.type" 240 | gTypes.each { 241 | if (it == childDevice.type) { 242 | def counter = ["name" : params.type, "counter" : counter++ ] 243 | } 244 | } 245 | } */ 246 | 247 | 248 | 249 | 250 | 251 | //def counter = childDevices.size() + 1 //TODO Fix counter for each type 252 | def dni = "pstuartDevice_$counter" // TODO create random string /guid 253 | def newDeviceName = "$params $counter" 254 | if (deviceName != params) { 255 | newDeviceName = deviceName 256 | } 257 | log.debug newDeviceName 258 | log.debug dni 259 | log.debug params 260 | log.trace "just about to add childe device" 261 | def childDevice = addChildDevice("pstuart", params, dni, null, [name:newDeviceName]) 262 | log.debug childDevice 263 | childDevice.off() 264 | return childDevice 265 | //return dni 266 | 267 | 268 | } 269 | 270 | def genericTypes() { 271 | def gTypes = [ 272 | "Generic Switch", 273 | "Generic Contact", 274 | "Generic Dimmer", 275 | ] 276 | return gTypes 277 | } -------------------------------------------------------------------------------- /smartapps/logger.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Xively Logger 3 | * 4 | * Author: patrick@patrickstuart.com 5 | * Date: 2014-04-14 6 | * 7 | * 8 | */ 9 | 10 | // Automatically generated. Make future change here. 11 | definition( 12 | name: "Xively Logger", 13 | namespace: "ps", 14 | author: "patrick@patrickstuart.com", 15 | description: "Xively Logger", 16 | category: "My Apps", 17 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 18 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 19 | 20 | preferences { 21 | section("Log devices...") { 22 | input "temperatures", "capability.temperatureMeasurement", title: "Temperatures", required:false, multiple: true 23 | input "humidities", "capability.relativeHumidityMeasurement", title: "Humidities", required:false, multiple: true 24 | input "contacts", "capability.contactSensor", title: "Contacts", required: false, multiple: true 25 | input "illuminances", "capability.illuminanceMeasurement", title: "Illuminances", required: false, multiple: true 26 | input "motions", "capability.motionSensor", title: "Motions", required: false, multiple: true 27 | input "switches", "capability.switch", title: "Switches", required: false, multiple: true 28 | input "batteries", "capability.battery", title: "Batteries", required:false, multiple: true 29 | input "thermostats", "capability.thermostat", title: "Select thermostat", required: false, multiple: true 30 | } 31 | 32 | section ("Xively Info") { 33 | input "xi_apikey", "text", title: "Xively API Key" 34 | input "xi_feed", "number", title: "Xively Feed ID" 35 | } 36 | } 37 | 38 | def installed() { 39 | initialize() 40 | } 41 | 42 | def updated() { 43 | unsubscribe() 44 | 45 | initialize() 46 | } 47 | 48 | def initialize() { 49 | state.clear() 50 | // if(!state.subscribe) { 51 | unschedule(checkSensors) 52 | //schedule("0 */15 * * * ?", "checkSensors") //Check every 5 mins 53 | schedule("0 */15 * * * ?", "checkSensors") 54 | subscribe(app, appTouch) 55 | // state.subscribe = true 56 | // } 57 | /* 58 | subscribe(temperatures, "temperature", handleTemperatureEvent) 59 | subscribe(humidities, "humidities", handlehumidityEvent) 60 | subscribe(contacts, "contact", handleContactEvent) 61 | subscribe(accelerations, "acceleration", handleAccelerationEvent) 62 | subscribe(motions, "motion", handleMotionEvent) 63 | subscribe(switches, "switch", handleSwitchEvent) 64 | subscribe(thermostats, "temperature", handleThermostatTemperature) 65 | subscribe(thermostats, "humidity", handleThermostatHumidity) 66 | */ 67 | 68 | //updateChannelInfo() 69 | //log.debug state.fieldMap 70 | } 71 | 72 | def appTouch(evt) { 73 | log.debug "appTouch: $evt" 74 | checkSensors() 75 | } 76 | 77 | /* 78 | def handleTemperatureEvent(evt) { 79 | log.debug "EVENT is $evt.displayName" 80 | log.debug "EVENT VALUE is $evt.value" 81 | //log.debug "Device ID is $evt.deviceId" 82 | 83 | logField("temperature", evt) { it.toString() } 84 | } 85 | 86 | def handlehumidityEvent(evt) { 87 | log.debug "EVENT is $evt.displayName" 88 | log.debug "EVENT VALUE is $evt.value" 89 | 90 | logField("humidity", evt) { it.toString() } 91 | } 92 | 93 | def handleContactEvent(evt) { 94 | log.debug "EVENT is $evt.displayName" 95 | log.debug "EVENT VALUE is $evt.value" 96 | logField("contact", evt) { it == "open" ? "1" : "0" } 97 | } 98 | 99 | def handleAccelerationEvent(evt) { 100 | log.debug "EVENT is $evt.displayName" 101 | log.debug "EVENT VALUE is $evt.value" 102 | logField("acceleration", evt) { it == "active" ? "1" : "0" } 103 | } 104 | 105 | def handleMotionEvent(evt) { 106 | log.debug "EVENT is $evt.displayName" 107 | log.debug "EVENT VALUE is $evt.value" 108 | logField("motion", evt) { it == "active" ? "1" : "0" } 109 | } 110 | 111 | def handleSwitchEvent(evt) { 112 | log.debug "EVENT is $evt.displayName" 113 | log.debug "EVENT VALUE is $evt.value" 114 | logField("switch", evt) { it == "on" ? "1" : "0" } 115 | } 116 | 117 | def handleThermostatTemperature(evt) { 118 | log.debug "EVENT is $evt.displayName" 119 | log.debug "EVENT VALUE is $evt.value" 120 | log.debug "TStat Temperature event: $evt.value" 121 | logField("thermostat.temperature", evt) { it.toString() } 122 | } 123 | 124 | def handleThermostatHumidity(evt) { 125 | log.debug "EVENT is $evt.displayName" 126 | log.debug "EVENT VALUE is $evt.value" 127 | log.debug "TStat Humidity event: $evt.value" 128 | logField("thermostat.humidity", evt) { it.toString() } 129 | } 130 | */ 131 | 132 | def checkSensors() { 133 | //updateChannelInfo() 134 | //settings.sensors.each{ k -> log.debug k } 135 | //https://graph.api.smartthings.com/api/hubs/idHashString/devices 136 | //https://graph.api.smartthings.com/device/listJson 137 | 138 | 139 | def logitems = [] 140 | //def states = [:] 141 | for (t in settings.temperatures) { 142 | //def deviceState = captureState(t) 143 | //log.debug "$t.name : $t.displayName : $deviceState.temperature" 144 | //def deviceID = t.id 145 | //states[deviceID] = deviceState 146 | //if (deviceID == "temperature") { 147 | //logitems.add([t.displayName, "temperature", deviceState.temperature] ) 148 | 149 | logitems.add([t.displayName, "temperature", Double.parseDouble(t.latestValue("temperature").toString())] ) 150 | state[t.displayName + ".temp"] = t.latestValue("temperature") 151 | 152 | //logField2("temperature", t.displayName, deviceState.temperature ) 153 | //} 154 | } 155 | for (t in settings.humidities) { 156 | //def deviceState = captureState(t) 157 | //def deviceID = t.id 158 | //states[deviceID] = deviceState 159 | //logitems.add([t.displayName, "humidity", deviceState.humidity]) 160 | logitems.add([t.displayName, "humidity", Double.parseDouble(t.latestValue("humidity").toString())] ) 161 | state[t.displayName + ".humidity"] = t.latestValue("humidity") 162 | } 163 | for (t in settings.batteries) { 164 | //def deviceState = captureState(t) 165 | //log.debug deviceState 166 | //def deviceID = t.id 167 | //states[deviceID] = deviceState 168 | //logitems.add([t.displayName, "battery", deviceState.battery]) 169 | 170 | logitems.add([t.displayName, "battery", Double.parseDouble(t.latestValue("battery").toString())] ) 171 | state[t.displayName + ".battery"] = t.latestValue("battery") 172 | } 173 | //log.debug settings.batteries.size() 174 | for (t in settings.contacts) { 175 | //def deviceState = captureState(t) 176 | //log.debug deviceState 177 | //def deviceID = t.id 178 | //states[deviceID] = deviceState 179 | //logitems.add([t.displayName, "contact", deviceState.contact]) 180 | 181 | logitems.add([t.displayName, "contact", t.latestValue("contact")] ) 182 | state[t.displayName + ".contact"] = t.latestValue("contact") 183 | } 184 | 185 | for (t in settings.motions) { 186 | //def deviceState = captureState(t) 187 | //log.debug deviceState 188 | //def deviceID = t.id 189 | //states[deviceID] = deviceState 190 | //logitems.add([t.displayName, "motion", deviceState.motion]) 191 | 192 | logitems.add([t.displayName, "motion", t.latestValue("motion")] ) 193 | state[t.displayName + ".motion"] = t.latestValue("motion") 194 | } 195 | 196 | for (t in settings.illuminances) { 197 | //def deviceState = captureState(t) 198 | //log.debug deviceState 199 | //def deviceID = t.id 200 | //states[deviceID] = deviceState 201 | //logitems.add([t.displayName, "motion", deviceState.motion]) 202 | //log.debug t.latestValue("illuminance") 203 | //log.debug (t.latestValue("illuminance") instanceof Double) 204 | log.debug t.displayName 205 | def x = new BigDecimal(t.latestValue("illuminance") ) // instanceof Double) 206 | log.debug x 207 | logitems.add([t.displayName, "illuminance", x] ) 208 | state[t.displayName + ".illuminance"] = x 209 | } 210 | 211 | for (t in settings.switches) { 212 | //def deviceState = captureState(t) 213 | //log.debug deviceState 214 | //def deviceID = t.id 215 | //states[deviceID] = deviceState 216 | //logitems.add([t.displayName, "contact", deviceState.switch]) 217 | 218 | logitems.add([t.displayName, "switch", t.latestValue("switch")] ) 219 | state[t.displayName + ".switch"] = t.latestValue("switch") 220 | } 221 | 222 | for (t in settings.thermostats) { 223 | //def deviceState = captureState(t) 224 | //def deviceID = t.id + "tstat" 225 | //states[deviceID] = deviceState 226 | //logitems.add([t.displayName, "thermostat.temperature", deviceState.coolingSetpoint]) 227 | //log.debug t 228 | logitems.add([t.displayName, "thermostat.coolingSetpoint", t.latestValue("coolingSetpoint")] ) 229 | state[t.displayName + ".coolingSetpoint"] = t.latestValue("coolingSetpoint") 230 | } 231 | 232 | //log.debug states 233 | //log.debug logitems 234 | logField2(logitems) 235 | 236 | } 237 | 238 | private getFieldMap(channelInfo) { 239 | def fieldMap = [:] 240 | channelInfo?.findAll { it.key?.startsWith("field") }.each { fieldMap[it.value?.trim()] = it.key } 241 | return fieldMap 242 | } 243 | 244 | /* 245 | private updateChannelInfo() { 246 | log.debug "Retrieving channel info for ${channelId}" 247 | 248 | def url = "http://api.thingspeak.com/channels/${channelId}/feed.json?key=${channelKey}&results=0" 249 | httpGet(url) { 250 | response -> 251 | if (response.status != 200 ) { 252 | log.debug "ThingSpeak data retrieval failed, status = ${response.status}" 253 | } else { 254 | state.channelInfo = response.data?.channel 255 | } 256 | log.debug response.data 257 | } 258 | 259 | state.fieldMap = getFieldMap(state.channelInfo) 260 | } 261 | */ 262 | 263 | /* 264 | private logField(capability, evt, Closure c) { 265 | log.debug "Got Log Request: $capability $evt.displayName $evt.value" 266 | def deviceName = evt.displayName.trim() + "." + capability 267 | def fieldNum = state.fieldMap[deviceName] 268 | if (!fieldNum) { 269 | log.debug "Device '${deviceName}' has no field" 270 | return 271 | } 272 | 273 | def value = c(evt.value) 274 | log.debug "Logging to channel ${channelId}, ${fieldNum}, value ${value}" 275 | 276 | def url = "http://api.thingspeak.com/update?key=${channelKey}&${fieldNum}=${value}" 277 | httpGet(url) { 278 | response -> 279 | if (response.status != 200 ) { 280 | log.debug "ThingSpeak logging failed, status = ${response.status}" 281 | } 282 | //log.debug response.data 283 | } 284 | } 285 | */ 286 | 287 | private logField2(logItems) { 288 | def fieldvalues = "" 289 | log.debug logItems 290 | 291 | /* 292 | logItems.each() { item -> 293 | //log.debug item[0] 294 | //log.debug item[1] 295 | //log.debug item[2] 296 | //log.debug "Got Log Item: $item.get(0) $item.get(1) $item.get(2)" 297 | 298 | def deviceName = item[0] + "." + item[1] 299 | //log.debug deviceName 300 | 301 | def fieldNum = state.fieldMap[deviceName] 302 | //log.debug fieldNum 303 | if (!fieldNum) { 304 | log.debug "Device '${deviceName}' has no field" 305 | return 306 | } 307 | //log.debug fieldNum 308 | 309 | 310 | 311 | //build string &fieldnum=value 312 | fieldvalues += "&$fieldNum=" +item[2] 313 | 314 | //def value = c(evt.value) 315 | log.debug "Logging to channel ${channelId}, ${fieldNum}, value ${item[2]}" 316 | 317 | } 318 | log.debug "String to send is: $fieldvalues" 319 | 320 | 321 | def url = "http://api.thingspeak.com/update?key=${channelKey}${fieldvalues}" 322 | log.debug url 323 | httpGet(url) { 324 | response -> 325 | if (response.status != 200 ) { 326 | log.debug "ThingSpeak logging failed, status = ${response.status}" 327 | } 328 | //log.debug response.data 329 | } 330 | 331 | */ 332 | 333 | def xivelyinfo = "" 334 | logItems.eachWithIndex() { item, i -> 335 | //need to build string {"id":"channel","current_value": "value"} plus comma 336 | def channelname = item[0].replace(" ","_") + "_" + item[1] 337 | xivelyinfo += "{\"id\":\"${channelname}\",\"current_value\":\"${item[2]}\"}" 338 | if (i.toInteger() + 1 < logItems.size()) 339 | { 340 | xivelyinfo += "," 341 | } 342 | 343 | } 344 | log.debug xivelyinfo 345 | def uri = "https://api.xively.com/v2/feeds/${xi_feed}.json" 346 | //def json = "{\"version\":\"1.0.0\",\"datastreams\":[{\"id\":\"${channel}\",\"current_value\":\"${value}\"}]}" 347 | def json = "{\"version\":\"1.0.0\",\"datastreams\":[${xivelyinfo} ]}" 348 | 349 | def headers = [ 350 | "X-ApiKey" : "${xi_apikey}" 351 | ] 352 | 353 | def params = [ 354 | uri: uri, 355 | headers: headers, 356 | body: json 357 | ] 358 | log.debug params.body 359 | httpPutJson(params) {response -> parseHttpResponse(response)} 360 | } 361 | 362 | def parseHttpResponse(response) { 363 | log.debug "HTTP Response: ${response}" 364 | } 365 | 366 | def captureState(theDevice) { 367 | // def deviceAttributes = theDevice.supportedAttributes 368 | def deviceAttrValue = [:] 369 | for ( attr in theDevice.supportedAttributes ) { 370 | def attrName = "${attr}" 371 | def attrValue = theDevice.currentValue(attrName) 372 | deviceAttrValue[attrName] = attrValue 373 | } 374 | return deviceAttrValue 375 | } 376 | -------------------------------------------------------------------------------- /smartapps/Ubi (Connect): -------------------------------------------------------------------------------- 1 | /** 2 | * ps_Ubi (Connect) 3 | * 4 | * Copyright 2014 patrick@patrickstuart.com 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * BIG SHOUT OUT TO THE ECOBEE Example for showing me how OAUTH flow can work... 16 | */ 17 | definition( 18 | name: "ps_Ubi (Connect)", 19 | namespace: "ps", 20 | author: "patrick@patrickstuart.com", 21 | description: "Connect to Ubi Portal", 22 | category: "My Apps", 23 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 24 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") { 25 | 26 | } 27 | 28 | 29 | preferences { 30 | page(name: "auth", title: "ubi", nextPage:"deviceList", content:"authPage", uninstall: true) 31 | page(name: "deviceList", title: "ubi", content:"ubiDeviceList", install:true) 32 | } 33 | 34 | mappings { 35 | path("/swapToken") { 36 | action: [ 37 | GET: "swapToken" 38 | ] 39 | } 40 | } 41 | 42 | def authPage() 43 | { 44 | log.debug "Version 1.0.2" 45 | log.debug "authPage()" 46 | 47 | if(!atomicState.accessToken) 48 | { 49 | log.debug "about to create access token" 50 | createAccessToken() 51 | atomicState.accessToken = state.accessToken 52 | } 53 | 54 | 55 | def description = "Required" 56 | def uninstallAllowed = false 57 | def oauthTokenProvided = false 58 | 59 | if(atomicState.authToken) 60 | { 61 | if(true) 62 | { 63 | description = "You are connected." 64 | uninstallAllowed = true 65 | oauthTokenProvided = true 66 | } 67 | else 68 | { 69 | description = "Required" 70 | oauthTokenProvided = false 71 | } 72 | } 73 | 74 | def redirectUrl = oauthInitUrl() 75 | 76 | log.debug "RedirectUrl = ${redirectUrl}" 77 | 78 | // get rid of next button until the user is actually auth'd 79 | log.debug "_______AUTH______ ${atomicState.authToken}" 80 | log.debug oauthTokenProvided 81 | 82 | if (!oauthTokenProvided) { 83 | 84 | return dynamicPage(name: "auth", title: "Login", nextPage:null, uninstall:uninstallAllowed) { 85 | section(){ 86 | paragraph "Tap below to log in to the ubi service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button." 87 | href url:redirectUrl, style:"embedded", required:true, title:"ubi", description:description 88 | } 89 | } 90 | 91 | } else { 92 | 93 | return dynamicPage(name: "auth", title: "Log In", nextPage:"deviceList", uninstall:uninstallAllowed) { 94 | section(){ 95 | paragraph "Tap Next to continue to setup your ubi." 96 | href url:redirectUrl, style:"embedded", state:"complete", title:"ubi", description:description 97 | } 98 | } 99 | 100 | } 101 | 102 | } 103 | 104 | def ubiDeviceList() 105 | { 106 | log.debug "ubiDeviceList()" 107 | 108 | def stats = getUbis() 109 | 110 | log.debug "device list: $stats" 111 | 112 | def p = dynamicPage(name: "deviceList", title: "Select Your Ubis", uninstall: true) { 113 | section(""){ 114 | paragraph "Tap below to see the list of Ubis available in your Ubi account and select the ones you want to connect to SmartThings." 115 | input(name: "ubis", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats]) 116 | } 117 | } 118 | 119 | log.debug "list p: $p" 120 | return p 121 | } 122 | 123 | def getUbis() 124 | { 125 | log.debug "getting device list" 126 | 127 | def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true}}' 128 | 129 | def deviceListParams = [ 130 | uri: "https://api.theubi.com", 131 | path: "/v2/ubi/list", 132 | headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], 133 | query: [format: 'json', body: requestBody] 134 | ] 135 | 136 | log.debug "_______AUTH______ ${atomicState.authToken}" 137 | log.debug "device list params: $deviceListParams" 138 | 139 | def stats = [:] 140 | httpGet(deviceListParams) { resp -> 141 | 142 | if(resp.status == 200) 143 | { 144 | resp.data.ubiList.each { stat -> 145 | def dni = [ app.id, stat.identifier ].join('.') 146 | stats[dni] = getUbiDisplayName(stat) 147 | } 148 | } 149 | else 150 | { 151 | log.debug "http status: ${resp.status}" 152 | 153 | //refresh the auth token 154 | if (resp.status == 500 && resp.data.status.code == 14) 155 | { 156 | log.debug "Storing the failed action to try later" 157 | data.action = "getUbis" 158 | log.debug "Refreshing your auth_token!" 159 | refreshAuthToken() 160 | } 161 | else 162 | { 163 | log.error "Authentication error, invalid authentication method, lack of credentials, etc." 164 | } 165 | } 166 | } 167 | 168 | log.debug "thermostats: $stats" 169 | 170 | return stats 171 | } 172 | 173 | 174 | def getUbiDisplayName(stat) 175 | { 176 | log.debug "getUbiDisplayName" 177 | if(stat?.name) 178 | { 179 | return stat.name.toString() 180 | } 181 | 182 | return ${stat.identifier} 183 | } 184 | 185 | def installed() { 186 | log.debug "Installed with settings: ${settings}" 187 | 188 | initialize() 189 | } 190 | 191 | def updated() { 192 | log.debug "Updated with settings: ${settings}" 193 | 194 | unsubscribe() 195 | initialize() 196 | } 197 | 198 | def initialize() { 199 | // TODO: subscribe to attributes, devices, locations, etc. 200 | log.debug "initialize" 201 | def devices = ubis.collect { dni -> 202 | 203 | def d = getChildDevice(dni) 204 | 205 | if(!d) 206 | { 207 | d = addChildDevice(getChildNamespace(), getChildName(), dni) 208 | log.debug "created ${d.displayName} with id $dni" 209 | } 210 | else 211 | { 212 | log.debug "found ${d.displayName} with id $dni already exists" 213 | } 214 | 215 | return d 216 | } 217 | 218 | log.debug "created ${devices.size()} ubis" 219 | 220 | def delete 221 | // Delete any that are no longer in settings 222 | if(!ubis) 223 | { 224 | log.debug "delete ubis" 225 | delete = getAllChildDevices() 226 | } 227 | else 228 | { 229 | delete = getChildDevices().findAll { !ubis.contains(it.deviceNetworkId) } 230 | } 231 | 232 | log.debug "deleting ${delete.size()} ubis" 233 | delete.each { deleteChildDevice(it.deviceNetworkId) } 234 | 235 | atomicState.thermostatData = [:] 236 | 237 | pollHandler() 238 | 239 | // schedule ("0 0/15 * 1/1 * ? *", pollHandler) 240 | } 241 | 242 | 243 | def oauthInitUrl() 244 | { 245 | log.debug "oauthInitUrl" 246 | // def oauth_url = "https://api.ecobee.com/authorize?response_type=code&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=http://localhost/&scope=smartRead,smartWrite&state=abc123" 247 | def stcid = getSmartThingsClientId(); 248 | 249 | atomicState.oauthInitState = UUID.randomUUID().toString() 250 | 251 | def oauthParams = [ 252 | response_type: "code", 253 | scope: "", 254 | client_id: stcid, 255 | client_secret: "07c98380-441c-4898-9880-eb3a70144832", 256 | state: atomicState.oauthInitState, 257 | redirect_uri: buildRedirectUrl() 258 | ] 259 | log.debug oauthParams 260 | return "http://test.theubi.com/oauth/authorize?" + toQueryString(oauthParams) 261 | } 262 | 263 | def buildRedirectUrl() 264 | { 265 | log.debug "buildRedirectUrl ${serverUrl}" 266 | // return serverUrl + "/api/smartapps/installations/${app.id}/token/${atomicState.accessToken}" 267 | return serverUrl + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken" 268 | } 269 | 270 | private refreshAuthToken() { 271 | log.debug "refreshing auth token" 272 | //debugEvent("refreshing OAUTH token", true) 273 | 274 | def stcid = getSmartThingsClientId() 275 | 276 | def refreshParams = [ 277 | method: 'POST', 278 | uri: "http://test.theubi.com", 279 | path: "/oauth/token", 280 | query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid], 281 | 282 | //data?.refreshToken 283 | ] 284 | 285 | log.debug "Patrick: refreshParams= $refreshParams" 286 | 287 | //changed to httpPost 288 | try{ 289 | def jsonMap 290 | httpPost(refreshParams) { resp -> 291 | 292 | if(resp.status == 200) 293 | { 294 | log.debug "Token refreshed...calling saved RestAction now!" 295 | 296 | //debugEvent("Token refreshed ... calling saved RestAction now!", true) 297 | 298 | log.debug resp 299 | 300 | jsonMap = resp.data 301 | 302 | if (resp.data) { 303 | 304 | log.debug resp.data 305 | //debugEvent ("Response = ${resp.data}", true) 306 | 307 | atomicState.refreshToken = resp?.data?.refresh_token 308 | atomicState.authToken = resp?.data?.access_token 309 | 310 | //debugEvent ("Refresh Token = ${atomicState.refreshToken}", true) 311 | //debugEvent ("OAUTH Token = ${atomicState.authToken}", true) 312 | 313 | if (data?.action && data?.action != "") { 314 | log.debug data.action 315 | 316 | "{data.action}"() 317 | 318 | //remove saved action 319 | data.action = "" 320 | } 321 | 322 | } 323 | data.action = "" 324 | } 325 | else 326 | { 327 | log.debug "refresh failed ${resp.status} : ${resp.status.code}" 328 | } 329 | } 330 | log.debug "Patrick: $jsonMap" 331 | atomicState.refreshToken = jsonMap.refresh_token 332 | atomicState.authToken = jsonMap.access_token 333 | } 334 | catch(Exception e) 335 | { 336 | log.debug "caught exception refreshing auth token: " + e 337 | } 338 | log.debug "_____AUTH_____ ${atomicState.authToken}" 339 | } 340 | 341 | def swapToken() 342 | { 343 | log.debug "swapping token: $params" 344 | log.debug "_____AUTH_____ ${atomicState.authToken}" 345 | //debugEvent ("swapping token: $params", true) 346 | 347 | def code = params.code 348 | def oauthState = params.state 349 | 350 | // TODO: verify oauthState == atomicState.oauthInitState 351 | 352 | 353 | 354 | def stcid = getSmartThingsClientId() 355 | 356 | def tokenParams = [ 357 | grant_type: "authorization_code", 358 | code: params.code, 359 | client_id: stcid, 360 | client_secret: "07c98380-441c-4898-9880-eb3a70144832", 361 | redirect_uri: buildRedirectUrl() 362 | ] 363 | 364 | 365 | def tokenUrl = "http://test.theubi.com/oauth/token?" + toQueryString(tokenParams) 366 | 367 | log.debug "SCOTT: swapping token $params" 368 | 369 | def jsonMap 370 | httpPost(uri:tokenUrl) { resp -> 371 | jsonMap = resp.data 372 | } 373 | 374 | log.debug "SCOTT: swapped token for $jsonMap" 375 | //debugEvent ("swapped token for $jsonMap", true) 376 | 377 | atomicState.refreshToken = jsonMap.refresh_token 378 | atomicState.authToken = jsonMap.access_token 379 | log.debug atomicState 380 | def html = """ 381 | 382 | 383 | 384 | 385 | Withings Connection 386 | 436 | 437 | 438 |
439 | 440 | connected device icon 441 | SmartThings logo 442 |

Your ubi Account is now connected to SmartThings!

443 |

Click 'Done' to finish setup.

444 |
445 | 446 | 447 | """ 448 | 449 | render contentType: 'text/html', data: html 450 | } 451 | 452 | 453 | 454 | def toQueryString(Map m) 455 | { 456 | return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") 457 | } 458 | 459 | def getServerUrl() { return "https://graph.api.smartthings.com" } 460 | def getSmartThingsClientId() { "55c9dd16-9268-4e1a-ab92-0fa5d876a987" } 461 | -------------------------------------------------------------------------------- /devicetypes/nest.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ps_Nest Thermostat 3 | * 4 | * Copyright 2014 Patrick Stuart 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | * 16 | * Original Author: dianoga7@3dgo.net 17 | * Author: patrick@patrickstuart.com 18 | * Original Code: https://github.com/smartthings-users/device-type.nest 19 | * 20 | * INSTALLATION 21 | * ========================================= 22 | * 1) Create a new device type (https://graph.api.smartthings.com/ide/devices) 23 | * Name: Nest 24 | * Author: dianoga7@3dgo.net 25 | * Capabilities: 26 | * Polling 27 | * Relative Humidity Measurement 28 | * Thermostat 29 | * Custom Attributes: 30 | * presence 31 | * Custom Commands: 32 | * away 33 | * present 34 | * setPresence 35 | * 36 | * 2) Create a new device (https://graph.api.smartthings.com/device/list) 37 | * Name: Your Choice 38 | * Device Network Id: Your Choice 39 | * Type: Nest (should be the last option) 40 | * Location: Choose the correct location 41 | * Hub/Group: Leave blank 42 | * 43 | * 3) Update device preferences 44 | * Click on the new device to see the details. 45 | * Click the edit button next to Preferences 46 | * Fill in your information. 47 | * To find your serial number, login to http://home.nest.com. Click on the thermostat 48 | * you want to control. Under settings, go to Technical Info. Your serial number is 49 | * the second item. 50 | * 51 | * 4) That's it, you're done. 52 | * 53 | * Copyright (C) 2013 Brian Steere 54 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 55 | * software and associated documentation files (the "Software"), to deal in the Software 56 | * without restriction, including without limitation the rights to use, copy, modify, 57 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 58 | * permit persons to whom the Software is furnished to do so, subject to the following 59 | * conditions: The above copyright notice and this permission notice shall be included 60 | * in all copies or substantial portions of the Software. 61 | * 62 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 63 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 64 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 65 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 66 | * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 67 | * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 68 | */ 69 | 70 | preferences { 71 | input("username", "text", title: "Username", description: "Your Nest username (usually an email address)") 72 | input("password", "password", title: "Password", description: "Your Nest password") 73 | input("serial", "text", title: "Serial #", description: "The serial number of your thermostat") 74 | } 75 | 76 | // for the UI 77 | metadata { 78 | definition (name: "Nest Thermostat", author: "patrick@patrickstuart.com") { 79 | capability "Polling" 80 | capability "Relative Humidity Measurement" 81 | capability "Thermostat" 82 | capability "Actuator" 83 | capability "Sensor" 84 | capability "Refresh" 85 | capability "Configuration" 86 | capability "Location Mode" 87 | 88 | attribute "presence", "string" 89 | 90 | command "away" 91 | command "present" 92 | command "setPresence" 93 | command "setTempUp" 94 | command "setTempDown" 95 | command "setTempUpHeat" 96 | command "setTempDownHeat" 97 | } 98 | simulator { 99 | 100 | } 101 | 102 | tiles { 103 | valueTile("temperature", "device.temperature", width: 1, height: 1, canChangeIcon: false) { 104 | state("temperature", label: '${currentValue}°', unit:"F", backgroundColors: [ 105 | [value: 31, color: "#153591"], 106 | [value: 44, color: "#1e9cbb"], 107 | [value: 59, color: "#90d2a7"], 108 | [value: 74, color: "#44b621"], 109 | [value: 84, color: "#f1d801"], 110 | [value: 95, color: "#d04e00"], 111 | [value: 96, color: "#bc2323"] 112 | ] 113 | ) 114 | } 115 | 116 | standardTile("TempUp", "device.button") { state "Cool", label:'${name}', action:"setTempUp", icon: "st.thermostat.thermostat-up", backgroundColor: '#003cec'} 117 | standardTile("TempDown", "device.button") { state "Cool", label:'${name}', action:"setTempDown", icon: "st.thermostat.thermostat-down", backgroundColor: '#003cec'} 118 | 119 | standardTile("TempUpHeat", "device.button") { state "Heat", label:'${name}', action:"setTempUpHeat", icon: "st.thermostat.thermostat-up", backgroundColor: '#e14902'} 120 | standardTile("TempDownHeat", "device.button") { state "Heat", label:'${name}', action:"setTempDownHeat", icon: "st.thermostat.thermostat-down", backgroundColor: '#e14902'} 121 | 122 | 123 | standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { 124 | state "heat", label:'', action:"thermostat.heat", icon: "st.thermostat.heat", backgroundColor: '#E14902' 125 | state "cool", label:'', action:"thermostat.cool", icon: "st.thermostat.cool", backgroundColor: '#003CEC' 126 | state "range", label:'', action:"thermostat.auto", icon: "st.thermostat.auto", backgroundColor: '#000000' 127 | state "off", label:'', action:"thermostat.off", icon: "st.thermostat.heating-cooling-off" 128 | } 129 | standardTile("thermostatFanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { 130 | state "auto", label:'', action:"thermostat.fanOn", icon: "st.thermostat.fan-auto" 131 | state "on", label:'', action:"thermostat.fanCirculate", icon: "st.thermostat.fan-on" 132 | state "circulate", label:'', action:"thermostat.fanAuto", icon: "st.thermostat.fan-circulate" 133 | } 134 | valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { 135 | state "default", label:'${currentValue}°', unit:"F", backgroundColor:"#ffffff", icon:"" 136 | } 137 | 138 | valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { 139 | state "default", label:'${currentValue}°', unit:"F", backgroundColor:"#ffffff", icon:"" 140 | } 141 | 142 | valueTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat") { 143 | state "default", label:'${currentValue}%', unit:"Humidity" 144 | } 145 | standardTile("presence", "device.presence", inactiveLabel: false, decoration: "flat") { 146 | state "present", label:'${name}', action:"away", icon: "st.Home.home2" 147 | state "away", label:'${name}', action:"present", icon: "st.Transportation.transportation5" 148 | } 149 | standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { 150 | state "default", action:"polling.poll", icon:"st.secondary.refresh" 151 | } 152 | main "temperature" 153 | details(["temperature", "thermostatMode", "thermostatFanMode", "TempUp", "TempDown", //"coolSliderControl", 154 | "coolingSetpoint", "TempUpHeat", "TempDownHeat", "heatingSetpoint", "humidity", "presence", "refresh"]) 155 | } 156 | } 157 | 158 | // parse events into attributes 159 | def parse(String description) { 160 | log.debug "Recieved a parse request for: $description" 161 | 162 | } 163 | 164 | def setTempUp() { 165 | def newtemp = device.currentValue("coolingSetpoint").toInteger() + 1 166 | sendEvent(name: 'coolingSetpoint', value: newtemp) 167 | setTargetTemp(newtemp) 168 | } 169 | 170 | def setTempDown() { 171 | def newtemp = device.currentValue("coolingSetpoint").toInteger() - 1 172 | sendEvent(name: 'coolingSetpoint', value: newtemp) 173 | setTargetTemp(newtemp) 174 | } 175 | 176 | def setTempUpHeat() { 177 | def newtemp = device.currentValue("heatingSetpoint").toInteger() + 1 178 | sendEvent(name: 'heatingSetpoint', value: newtemp) 179 | setCoolingSetpoint(newtemp) 180 | } 181 | 182 | def setTempDownHeat() { 183 | def newtemp = device.currentValue("heatingSetpoint").toInteger() - 1 184 | sendEvent(name: 'heatingSetpoint', value: newtemp) 185 | setHeatingSetpoint(newtemp) 186 | } 187 | 188 | 189 | // handle commands 190 | def setHeatingSetpoint(temp) { 191 | setTargetTemp(temp) 192 | } 193 | 194 | def setCoolingSetpoint(temp) { 195 | setTargetTemp(temp) 196 | } 197 | 198 | def setTargetTemp(temp) { 199 | def tmode = device.currentValue("thermostatMode") 200 | if (tmode == "range") 201 | { 202 | log.debug "in auto mode about to send to api" 203 | api('temperature', [ 204 | 'target_change_pending': true, 205 | 'target_temperature_low': fToC(device.currentValue("heatingSetpoint")), 206 | 'target_temperature_high': fToC(device.currentValue("coolingSetpoint")), 207 | 'target_temperature': fToC(temp) 208 | ]) { } 209 | } 210 | else 211 | { 212 | if (tmode == "heat") 213 | { 214 | api('temperature', ['target_change_pending': true, 'target_temperature': fToC(device.currentValue("heatingSetpoint"))]) { 215 | } 216 | } 217 | else if (tmode == "cool") 218 | { 219 | api('temperature', ['target_change_pending': true, 'target_temperature': fToC(device.currentValue("coolingSetpoint"))]) { 220 | } 221 | } 222 | } 223 | } 224 | 225 | 226 | 227 | 228 | def off() { 229 | setThermostatMode('off') 230 | log.debug "off" 231 | } 232 | 233 | def heat() { 234 | setThermostatMode('heat') 235 | log.debug "heat" 236 | } 237 | 238 | def emergencyHeat() { 239 | setThermostatMode('heat') 240 | log.debug "eheat" 241 | } 242 | 243 | def cool() { 244 | setThermostatMode('cool') 245 | log.debug "cool" 246 | } 247 | 248 | def auto() { 249 | def tmode = device.currentValue("thermostatMode") 250 | setThermostatMode(tmode) 251 | log.debug "auto Mode hit" 252 | } 253 | 254 | def setThermostatMode(mode) { 255 | log.debug "mode is $mode" 256 | // rotate through 4 options heat,cool, range, off 257 | switch (mode) 258 | { 259 | case 'heat' : 260 | mode = "cool" 261 | break 262 | case 'cool' : 263 | mode = "range" 264 | break 265 | case 'range' : 266 | mode = "off" 267 | break 268 | case 'off' : 269 | mode = "heat" 270 | break 271 | default : 272 | mode = "off" 273 | break 274 | } 275 | 276 | api('thermostat_mode', ['target_change_pending': true, 'target_temperature_type': mode]) { 277 | sendEvent(name: 'thermostatMode', value: mode) 278 | } 279 | } 280 | 281 | def fanOn() { 282 | setThermostatFanMode('on') 283 | } 284 | 285 | def fanAuto() { 286 | setThermostatFanMode('auto') 287 | } 288 | 289 | def fanCirculate() { 290 | setThermostatFanMode('circulate') 291 | } 292 | 293 | def setThermostatFanMode(mode) { 294 | def modes = [ 295 | on: ['fan_mode': 'on'], 296 | auto: ['fan_mode': 'auto'], 297 | circulate: ['fan_mode': 'duty-cycle', 'fan_duty_cycle': 900] 298 | ] 299 | 300 | api('fan_mode', modes.getAt(mode)) { 301 | sendEvent(name: 'thermostatFanMode', value: mode) 302 | } 303 | } 304 | 305 | def away() { 306 | log.debug "Away Nest" 307 | setPresence('away') 308 | } 309 | 310 | def present() { 311 | log.debug "Home Nest" 312 | setPresence('present') 313 | } 314 | 315 | def setPresence(status) { 316 | log.debug "Status: $status" 317 | api('presence', ['away': status == 'away', 'away_timestamp': new Date().getTime(), 'away_setter': 0]) { 318 | sendEvent(name: 'presence', value: status) 319 | } 320 | } 321 | 322 | def refresh() { 323 | log.debug "nest refresh" 324 | } 325 | 326 | def poll() { 327 | log.debug "Executing 'poll'" 328 | api('status', []) { 329 | log.debug data.shared 330 | data.device = it.data.device.getAt(settings.serial) 331 | data.shared = it.data.shared.getAt(settings.serial) 332 | data.structureId = it.data.link.getAt(settings.serial).structure.tokenize('.')[1] 333 | data.structure = it.data.structure.getAt(data.structureId) 334 | 335 | data.device.fan_mode = data.device.fan_mode == 'duty-cycle'? 'circulate' : data.device.fan_mode 336 | data.structure.away = data.structure.away? 'away' : 'present' 337 | 338 | sendEvent(name: 'humidity', value: data.device.current_humidity) 339 | sendEvent(name: 'temperature', value: cToF((data.shared.current_temperature) as Double).round(), state: data.device.target_temperature_type) 340 | sendEvent(name: 'thermostatFanMode', value: data.device.fan_mode) 341 | sendEvent(name: 'thermostatMode', value: data.shared.target_temperature_type) 342 | //if temp type is ranged otherwise use target_temp 343 | if (data.shared.target_temperature_type == "range") 344 | { 345 | sendEvent(name: 'coolingSetpoint', value: cToF((data.shared.target_temperature_high) as Double).round()) 346 | sendEvent(name: 'heatingSetpoint', value: cToF((data.shared.target_temperature_low) as Double).round()) 347 | } 348 | else if (data.shared.target_temperature_type == "cool") 349 | { 350 | sendEvent(name: 'coolingSetpoint', value: cToF((data.shared.target_temperature) as Double).round()) 351 | } 352 | else if (data.shared.target_temperature_type == "heat") 353 | { 354 | sendEvent(name: 'heatingSetpoint', value: cToF((data.shared.target_temperature) as Double).round()) 355 | } 356 | else 357 | { 358 | sendEvent(name: 'coolingSetpoint', value: 0) //changed for auto mode for heat/cool? 359 | sendEvent(name: 'heatingSetpoint', value: 0) 360 | } 361 | 362 | sendEvent(name: 'presence', value: data.structure.away) 363 | 364 | //ambient_temperature_f? 365 | //has_leaf? 366 | //name = tstat name 367 | } 368 | } 369 | 370 | def api(method, args = [], success = {}) { 371 | if(!isLoggedIn()) { 372 | log.debug "Need to login" 373 | login(method, args, success) 374 | return 375 | } 376 | 377 | def methods = [ 378 | 'status': [uri: "/v2/mobile/${data.auth.user}", type: 'get'], 379 | 'fan_mode': [uri: "/v2/put/device.${settings.serial}", type: 'post'], 380 | 'thermostat_mode': [uri: "/v2/put/shared.${settings.serial}", type: 'post'], 381 | 'temperature': [uri: "/v2/put/shared.${settings.serial}", type: 'post'], 382 | 'presence': [uri: "/v2/put/structure.${data.structureId}", type: 'post'] 383 | ] 384 | 385 | def request = methods.getAt(method) 386 | log.debug method 387 | //Potentially this is the only spot that triggers from Mode Changes 388 | log.debug "Current mode = ${location.mode}" 389 | 390 | log.debug "Logged in" 391 | log.debug "Arguments to send are : $args" 392 | doRequest(request.uri, args, request.type, success) 393 | } 394 | 395 | // Need to be logged in before this is called. So don't call this. Call api. 396 | def doRequest(uri, args, type, success) { 397 | log.debug "Calling $type : $uri : $args" 398 | 399 | if(uri.charAt(0) == '/') { 400 | uri = "${data.auth.urls.transport_url}${uri}" 401 | } 402 | 403 | def params = [ 404 | uri: uri, 405 | headers: [ 406 | 'X-nl-protocol-version': 1, 407 | 'X-nl-user-id': data.auth.userid, 408 | 'Authorization': "Basic ${data.auth.access_token}" 409 | ], 410 | body: args 411 | ] 412 | 413 | try { 414 | if(type == 'post') { 415 | httpPostJson(params, success) 416 | } else if (type == 'get') { 417 | httpGet(params, success) 418 | } 419 | } catch (Throwable e) { 420 | login() 421 | } 422 | } 423 | 424 | def login(method = null, args = [], success = {}) { 425 | def params = [ 426 | uri: 'https://home.nest.com/user/login', 427 | body: [username: settings.username, password: settings.password] 428 | ] 429 | 430 | httpPost(params) {response -> 431 | data.auth = response.data 432 | data.auth.expires_in = Date.parse('EEE, dd-MMM-yyyy HH:mm:ss z', response.data.expires_in).getTime() 433 | log.debug data.auth 434 | 435 | api(method, args, success) 436 | } 437 | } 438 | 439 | def isLoggedIn() { 440 | if(!data.auth) { 441 | log.debug "No data.auth" 442 | return false 443 | } 444 | 445 | def now = new Date().getTime(); 446 | return data.auth.expires_in > now 447 | } 448 | 449 | def cToF(temp) { 450 | return temp * 1.8 + 32 451 | } 452 | 453 | def fToC(temp) { 454 | return (temp - 32) / 1.8 455 | } 456 | 457 | def configure() { 458 | log.debug "Configure() nest hit" 459 | } 460 | 461 | def switchMode() { 462 | def currentMode = device.currentState("thermostatMode")?.value 463 | log.debug "nest current mode is $currentMode" 464 | } 465 | -------------------------------------------------------------------------------- /devicetypes/smartsense multi + graph.groovy: -------------------------------------------------------------------------------- 1 | metadata { 2 | 3 | definition (name: "SmartSense Multi + Graph", namespace: "smartthings", author: "SmartThings") { 4 | capability "Three Axis" 5 | capability "Contact Sensor" 6 | capability "Acceleration Sensor" 7 | capability "Signal Strength" 8 | capability "Temperature Measurement" 9 | capability "Sensor" 10 | capability "Battery" 11 | } 12 | 13 | preferences { 14 | input("TempAdjust", "Double", title: "Temp Adjust", description: "Adjust your temp reading +/-") 15 | } 16 | 17 | simulator { 18 | status "open": "zone report :: type: 19 value: 0031" 19 | status "closed": "zone report :: type: 19 value: 0030" 20 | 21 | status "acceleration": "acceleration: 1, rssi: 0, lqi: 0" 22 | status "no acceleration": "acceleration: 0, rssi: 0, lqi: 0" 23 | 24 | for (int i = 10; i <= 50; i += 10) { 25 | status "temp ${i}C": "contactState: 0, accelerationState: 0, temp: $i C, battery: 100, rssi: 100, lqi: 255" 26 | } 27 | 28 | status "temp 72F": "contactSate: 0, accelerationState: 0, temp: 72 F, battery: 100, rssi: 100, lqi:255" 29 | 30 | // kinda hacky because it depends on how it is installed 31 | status "x,y,z: 0,0,0": "x: 0, y: 0, z: 0, rssi: 100, lqi: 255" 32 | status "x,y,z: 1000,0,0": "x: 1000, y: 0, z: 0, rssi: 100, lqi: 255" 33 | status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0, rssi: 100, lqi: 255" 34 | status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000, rssi: 100, lqi: 255" 35 | 36 | 37 | } 38 | 39 | tiles { 40 | standardTile("contact", "device.contact", width: 2, height: 2) { 41 | state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") 42 | state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") 43 | } 44 | standardTile("acceleration", "device.acceleration") { 45 | state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") 46 | state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") 47 | } 48 | valueTile("temperature", "device.temperature") { 49 | state("temperature", label:'${currentValue}°', 50 | backgroundColors:[ 51 | [value: 31, color: "#153591"], 52 | [value: 44, color: "#1e9cbb"], 53 | [value: 59, color: "#90d2a7"], 54 | [value: 74, color: "#44b621"], 55 | [value: 84, color: "#f1d801"], 56 | [value: 95, color: "#d04e00"], 57 | [value: 96, color: "#bc2323"] 58 | ] 59 | ) 60 | } 61 | chartTile(name: "temperatureChart", attribute: "device.temperature") 62 | valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false) { 63 | state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") 64 | } 65 | valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { 66 | state "battery", label:'${currentValue}% battery', unit:""/*, backgroundColors:[ 67 | [value: 5, color: "#BC2323"], 68 | [value: 10, color: "#D04E00"], 69 | [value: 15, color: "#F1D801"], 70 | [value: 16, color: "#FFFFFF"] 71 | ]*/ 72 | } 73 | /* 74 | valueTile("lqi", "device.lqi", decoration: "flat", inactiveLabel: false) { 75 | state "lqi", label:'${currentValue}% signal', unit:"" 76 | } 77 | */ 78 | 79 | main(["contact", "acceleration", "temperature"]) 80 | details(["contact", "acceleration", "temperature", "3axis", "battery", /*"lqi",*/ "temperatureChart"]) 81 | } 82 | } 83 | 84 | def parse(String description) { 85 | log.debug "Parse got this info: $description" 86 | def results 87 | 88 | if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { 89 | results = parseSingleMessage(description) 90 | } 91 | else if (description == 'updated') { 92 | //TODO is there a better way to handle this like the other device types? 93 | results = parseOtherMessage(description) 94 | } 95 | else { 96 | results = parseMultiSensorMessage(description) 97 | } 98 | log.debug "Parse returned $results.descriptionText" 99 | return results 100 | 101 | } 102 | 103 | private Map parseSingleMessage(description) { 104 | 105 | def name = parseName(description) 106 | def value = parseValue(description) 107 | def linkText = getLinkText(device) 108 | def descriptionText = parseDescriptionText(linkText, value, description) 109 | def handlerName = value == 'open' ? 'opened' : value 110 | def isStateChange = isStateChange(device, name, value) 111 | 112 | def results = [ 113 | name: name, 114 | value: value, 115 | unit: null, 116 | linkText: linkText, 117 | descriptionText: descriptionText, 118 | handlerName: handlerName, 119 | isStateChange: isStateChange, 120 | displayed: displayed(description, isStateChange) 121 | ] 122 | log.debug "Parse results for $device: $results" 123 | 124 | results 125 | } 126 | 127 | //TODO this just to handle 'updated' for now - investigate better way to do this 128 | private Map parseOtherMessage(description) { 129 | def name = null 130 | def value = description 131 | def linkText = getLinkText(device) 132 | def descriptionText = description 133 | def handlerName = description 134 | def isStateChange = isStateChange(device, name, value) 135 | 136 | def results = [ 137 | name: name, 138 | value: value, 139 | unit: null, 140 | linkText: linkText, 141 | descriptionText: descriptionText, 142 | handlerName: handlerName, 143 | isStateChange: isStateChange, 144 | displayed: displayed(description, isStateChange) 145 | ] 146 | log.debug "Parse results for $device: $results" 147 | 148 | results 149 | } 150 | 151 | private List parseMultiSensorMessage(description) { 152 | def results = [] 153 | if (isAccelerationMessage(description)) { 154 | results = parseAccelerationMessage(description) 155 | } 156 | else if (isContactMessage(description)) { 157 | results = parseContactMessage(description) 158 | } 159 | else if (isRssiLqiMessage(description)) { 160 | results = parseRssiLqiMessage(description) 161 | } 162 | else if (isOrientationMessage(description)) { 163 | results = parseOrientationMessage(description) 164 | } 165 | 166 | results 167 | } 168 | 169 | private List parseAccelerationMessage(String description) { 170 | def results = [] 171 | def parts = description.split(',') 172 | parts.each { part -> 173 | part = part.trim() 174 | if (part.startsWith('acceleration:')) { 175 | results << getAccelerationResult(part, description) 176 | } 177 | else if (part.startsWith('rssi:')) { 178 | results << getRssiResult(part, description) 179 | } 180 | else if (part.startsWith('lqi:')) { 181 | results << getLqiResult(part, description) 182 | } 183 | } 184 | 185 | results 186 | } 187 | 188 | private List parseContactMessage(String description) { 189 | def results = [] 190 | def parts = description.split(',') 191 | parts.each { part -> 192 | part = part.trim() 193 | if (part.startsWith('contactState:')) { 194 | results << getContactResult(part, description) 195 | } 196 | else if (part.startsWith('accelerationState:')) { 197 | results << getAccelerationResult(part, description) 198 | } 199 | else if (part.startsWith('temp:')) { 200 | results << getTempResult(part, description) 201 | } 202 | else if (part.startsWith('battery:')) { 203 | results << getBatteryResult(part, description) 204 | } 205 | else if (part.startsWith('rssi:')) { 206 | results << getRssiResult(part, description) 207 | } 208 | else if (part.startsWith('lqi:')) { 209 | results << getLqiResult(part, description) 210 | } 211 | } 212 | 213 | results 214 | } 215 | 216 | private List parseOrientationMessage(String description) { 217 | def results = [] 218 | def xyzResults = [x: 0, y: 0, z: 0] 219 | def parts = description.split(',') 220 | parts.each { part -> 221 | part = part.trim() 222 | if (part.startsWith('x:')) { 223 | def unsignedX = part.split(":")[1].trim().toInteger() 224 | def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX 225 | xyzResults.x = signedX 226 | } 227 | else if (part.startsWith('y:')) { 228 | def unsignedY = part.split(":")[1].trim().toInteger() 229 | def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY 230 | xyzResults.y = signedY 231 | } 232 | else if (part.startsWith('z:')) { 233 | def unsignedZ = part.split(":")[1].trim().toInteger() 234 | def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ 235 | xyzResults.z = signedZ 236 | } 237 | else if (part.startsWith('rssi:')) { 238 | results << getRssiResult(part, description) 239 | } 240 | else if (part.startsWith('lqi:')) { 241 | results << getLqiResult(part, description) 242 | } 243 | } 244 | 245 | results << getXyzResult(xyzResults, description) 246 | 247 | results 248 | } 249 | 250 | private List parseRssiLqiMessage(String description) { 251 | def results = [] 252 | // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" 253 | def parts = description.split(',') 254 | parts.each { part -> 255 | part = part.trim() 256 | if (part.startsWith('lastHopRssi:')) { 257 | results << getRssiResult(part, description, true) 258 | } 259 | else if (part.startsWith('lastHopLqi:')) { 260 | results << getLqiResult(part, description, true) 261 | } 262 | else if (part.startsWith('rssi:')) { 263 | results << getRssiResult(part, description) 264 | } 265 | else if (part.startsWith('lqi:')) { 266 | results << getLqiResult(part, description) 267 | } 268 | } 269 | 270 | results 271 | } 272 | 273 | private getContactResult(part, description) { 274 | def name = "contact" 275 | def value = part.endsWith("1") ? "open" : "closed" 276 | def handlerName = value == 'open' ? 'opened' : value 277 | def linkText = getLinkText(device) 278 | def descriptionText = "$linkText was $handlerName" 279 | def isStateChange = isStateChange(device, name, value) 280 | 281 | [ 282 | name: name, 283 | value: value, 284 | unit: null, 285 | linkText: linkText, 286 | descriptionText: descriptionText, 287 | handlerName: handlerName, 288 | isStateChange: isStateChange, 289 | displayed: displayed(description, isStateChange) 290 | ] 291 | } 292 | 293 | private getAccelerationResult(part, description) { 294 | def name = "acceleration" 295 | def value = part.endsWith("1") ? "active" : "inactive" 296 | def linkText = getLinkText(device) 297 | def descriptionText = "$linkText was $value" 298 | def isStateChange = isStateChange(device, name, value) 299 | 300 | [ 301 | name: name, 302 | value: value, 303 | unit: null, 304 | linkText: linkText, 305 | descriptionText: descriptionText, 306 | handlerName: value, 307 | isStateChange: isStateChange, 308 | displayed: displayed(description, isStateChange) 309 | ] 310 | } 311 | 312 | private getTempResult(part, description) { 313 | def name = "temperature" 314 | def temperatureScale = getTemperatureScale() 315 | // temp is mV get celcuis is mv / 10 then convert to F 316 | //def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) 317 | def value = part.split(":")[1].toDouble() / 10 // Celcius 318 | if (temperatureScale == "F") 319 | { value = value * 1.8 + 32 } 320 | log.debug TempAdjust 321 | value = (value.toDouble() + TempAdjust.toDouble()).round().toString() // PS changed to +/- temp adjustment 322 | log.debug value 323 | def linkText = getLinkText(device) 324 | def descriptionText = "$linkText was $value°$temperatureScale" 325 | def isStateChange = isTemperatureStateChange(device, name, value) 326 | 327 | storeData("temperature", value) 328 | 329 | [ 330 | name: name, 331 | value: value, 332 | unit: temperatureScale, 333 | linkText: linkText, 334 | descriptionText: descriptionText, 335 | handlerName: name, 336 | isStateChange: isStateChange, 337 | displayed: displayed(description, isStateChange) 338 | ] 339 | } 340 | 341 | private getXyzResult(results, description) { 342 | def name = "threeAxis" 343 | def value = "${results.x},${results.y},${results.z}" 344 | def linkText = getLinkText(device) 345 | def descriptionText = "$linkText was $value" 346 | def isStateChange = isStateChange(device, name, value) 347 | 348 | [ 349 | name: name, 350 | value: value, 351 | unit: null, 352 | linkText: linkText, 353 | descriptionText: descriptionText, 354 | handlerName: name, 355 | isStateChange: isStateChange, 356 | displayed: false 357 | ] 358 | } 359 | 360 | private getBatteryResult(part, description) { 361 | def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null 362 | def name = "battery" 363 | def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) 364 | def unit = "%" 365 | def linkText = getLinkText(device) 366 | def descriptionText = "$linkText Battery was ${value}${unit}" 367 | def isStateChange = isStateChange(device, name, value) 368 | 369 | [ 370 | name: name, 371 | value: value, 372 | unit: unit, 373 | linkText: linkText, 374 | descriptionText: descriptionText, 375 | handlerName: name, 376 | isStateChange: isStateChange, 377 | displayed: false 378 | ] 379 | } 380 | 381 | private getRssiResult(part, description, lastHop=false) { 382 | def name = lastHop ? "lastHopRssi" : "rssi" 383 | def valueString = part.split(":")[1].trim() 384 | def value = (Integer.parseInt(valueString) - 128).toString() 385 | def linkText = getLinkText(device) 386 | def descriptionText = "$linkText was $value dBm" 387 | if (lastHop) { 388 | descriptionText += " on the last hop" 389 | } 390 | def isStateChange = isStateChange(device, name, value) 391 | 392 | [ 393 | name: name, 394 | value: value, 395 | unit: "dBm", 396 | linkText: linkText, 397 | descriptionText: descriptionText, 398 | handlerName: null, 399 | isStateChange: isStateChange, 400 | displayed: false 401 | ] 402 | } 403 | 404 | /** 405 | * Use LQI (Link Quality Indicator) as a measure of signal strength. The values 406 | * are 0 to 255 (0x00 to 0xFF) and higher values represent higher signal 407 | * strength. Return as a percentage of 255. 408 | * 409 | * Note: To make the signal strength indicator more accurate, we could combine 410 | * LQI with RSSI. 411 | */ 412 | private getLqiResult(part, description, lastHop=false) { 413 | def name = lastHop ? "lastHopLqi" : "lqi" 414 | def valueString = part.split(":")[1].trim() 415 | def percentageOf = 255 416 | def value = Math.round((Integer.parseInt(valueString) / percentageOf * 100)).toString() 417 | def unit = "%" 418 | def linkText = getLinkText(device) 419 | def descriptionText = "$linkText Signal (LQI) was: ${value}${unit}" 420 | if (lastHop) { 421 | descriptionText += " on the last hop" 422 | } 423 | def isStateChange = isStateChange(device, name, value) 424 | 425 | [ 426 | name: name, 427 | value: value, 428 | unit: unit, 429 | linkText: linkText, 430 | descriptionText: descriptionText, 431 | handlerName: null, 432 | isStateChange: isStateChange, 433 | displayed: false 434 | ] 435 | } 436 | 437 | private Boolean isAccelerationMessage(String description) { 438 | // "acceleration: 1, rssi: 91, lqi: 255" 439 | description ==~ /acceleration:.*rssi:.*lqi:.*/ 440 | } 441 | 442 | private Boolean isContactMessage(String description) { 443 | // "contactState: 1, accelerationState: 0, temp: 14.4 C, battery: 28, rssi: 59, lqi: 255" 444 | description ==~ /contactState:.*accelerationState:.*temp:.*battery:.*rssi:.*lqi:.*/ 445 | } 446 | 447 | private Boolean isRssiLqiMessage(String description) { 448 | // "lastHopRssi: 91, lastHopLqi: 255, rssi: 91, lqi: 255" 449 | description ==~ /lastHopRssi:.*lastHopLqi:.*rssi:.*lqi:.*/ 450 | } 451 | 452 | private Boolean isOrientationMessage(String description) { 453 | // "x: 0, y: 33, z: 1017, rssi: 102, lqi: 255" 454 | description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/ 455 | } 456 | 457 | private String parseName(String description) { 458 | if (isSupportedDescription(description)) { 459 | return "contact" 460 | } 461 | null 462 | } 463 | 464 | private String parseValue(String description) { 465 | if (!isSupportedDescription(description)) { 466 | return description 467 | } 468 | else if (zigbee.translateStatusZoneType19(description)) { 469 | return "open" 470 | } 471 | else { 472 | return "closed" 473 | } 474 | } 475 | 476 | private parseDescriptionText(String linkText, String value, String description) { 477 | if (!isSupportedDescription(description)) { 478 | return value 479 | } 480 | 481 | value ? "$linkText was ${value == 'open' ? 'opened' : value}" : "" 482 | } 483 | 484 | def getVisualizationData(attribute) { 485 | log.debug "getChartData for $attribute" 486 | def keyBase = "measure.${attribute}" 487 | log.debug "getChartData state = $state" 488 | 489 | def dateBuckets = state[keyBase] 490 | 491 | //convert to the right format 492 | def results = dateBuckets?.sort{it.key}.collect {[ 493 | date: Date.parse("yyyy-MM-dd", it.key), 494 | average: it.value.average, 495 | min: it.value.min, 496 | max: it.value.max 497 | ]} 498 | 499 | log.debug "getChartData results = $results" 500 | results 501 | } 502 | 503 | private getKeyFromDate(date = new Date()){ 504 | date.format("yyyy-MM-dd") 505 | } 506 | 507 | private storeData(attribute, value) { 508 | log.debug "storeData initial state: $state" 509 | def keyBase = "measure.${attribute}" 510 | def numberValue = value.toBigDecimal() 511 | 512 | // create bucket if it doesn't exist 513 | if(!state[keyBase]) { 514 | state[keyBase] = [:] 515 | log.debug "storeData - attribute not found. New state: $state" 516 | } 517 | 518 | def dateString = getKeyFromDate() 519 | if(!state[keyBase][dateString]) { 520 | //no date bucket yet, fill with initial values 521 | state[keyBase][dateString] = [:] 522 | state[keyBase][dateString].average = numberValue 523 | state[keyBase][dateString].runningSum = numberValue 524 | state[keyBase][dateString].runningCount = 1 525 | state[keyBase][dateString].min = numberValue 526 | state[keyBase][dateString].max = numberValue 527 | 528 | log.debug "storeData date bucket not found. New state: $state" 529 | 530 | // remove old buckets 531 | def old = getKeyFromDate(new Date() - 10) 532 | state[keyBase].findAll { it.key < old }.collect { it.key }.each { state[keyBase].remove(it) } 533 | } else { 534 | //re-calculate average/min/max for this bucket 535 | state[keyBase][dateString].runningSum = (state[keyBase][dateString].runningSum.toBigDecimal()) + numberValue 536 | state[keyBase][dateString].runningCount = state[keyBase][dateString].runningCount.toInteger() + 1 537 | state[keyBase][dateString].average = state[keyBase][dateString].runningSum.toBigDecimal() / state[keyBase][dateString].runningCount.toInteger() 538 | 539 | log.debug "storeData after average calculations. New state: $state" 540 | 541 | if(state[keyBase][dateString].min == null) { 542 | state[keyBase][dateString].min = numberValue 543 | } else if (numberValue < state[keyBase][dateString].min.toBigDecimal()) { 544 | state[keyBase][dateString].min = numberValue 545 | } 546 | if(state[keyBase][dateString].max == null) { 547 | state[keyBase][dateString].max = numberValue 548 | } else if (numberValue > state[keyBase][dateString].max.toBigDecimal()) { 549 | state[keyBase][dateString].max = numberValue 550 | } 551 | } 552 | log.debug "storeData after min/max calculations. New state: $state" 553 | } 554 | -------------------------------------------------------------------------------- /devicetypes/zenwithin/zen-thermostat-new-ui.src/zen-thermostat-new-ui.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Zen Thermostat 3 | * 4 | * Author: Zen Within 5 | * Date: 2015-02-21 6 | */ 7 | metadata { 8 | definition (name: "Zen Thermostat New UI", namespace: "zenwithin", author: "ZenWithin") { 9 | capability "Actuator" 10 | capability "Thermostat" 11 | capability "Temperature Measurement" 12 | capability "Configuration" 13 | capability "Refresh" 14 | capability "Sensor" 15 | capability "Switch Level" 16 | 17 | fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019" 18 | 19 | //attribute "temperatureUnit", "number" 20 | 21 | command "setpointUp" 22 | command "setpointDown" 23 | 24 | command "setCelsius" 25 | command "setFahrenheit" 26 | 27 | // To please some of the thermostat SmartApps 28 | command "poll" 29 | } 30 | 31 | // simulator metadata 32 | simulator { } 33 | 34 | tiles (scale:2) { 35 | multiAttributeTile(name:"richtstat", type:"lighting", width:6, height:4) { 36 | tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { 37 | attributeState "temperature", label: '${currentValue}', backgroundColor:"#0a1ec2" 38 | } 39 | tileAttribute("statusText", key: "SECONDARY_CONTROL") { 40 | attributeState "statusText", label: '${currentValue}' 41 | } 42 | /* 43 | tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { 44 | attributeState("default", label:'${currentValue}', unit:"dF") 45 | } 46 | tileAttribute("device.temperature", key: "VALUE_CONTROL") { 47 | attributeState("default", action: "setTemperature") 48 | } 49 | tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { 50 | attributeState("default", label:'${currentValue}%', unit:"%") 51 | } 52 | tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { 53 | attributeState("idle", backgroundColor:"#44b621") 54 | attributeState("heating", backgroundColor:"#ffa81e") 55 | attributeState("cooling", backgroundColor:"#269bd2") 56 | } 57 | tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { 58 | attributeState("off", label:'${name}') 59 | attributeState("heat", label:'${name}') 60 | attributeState("cool", label:'${name}') 61 | attributeState("auto", label:'${name}') 62 | } 63 | tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { 64 | attributeState("default", label:'${currentValue}', unit:"dF") 65 | } 66 | tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { 67 | attributeState("default", label:'${currentValue}', unit:"dF") 68 | } 69 | 70 | /* 71 | tileAttribute("statusText", key: "VALUE_CONTROL") { 72 | attributeState "UpandDown", label: '${currentValue}' 73 | } 74 | * Currently broken on android 75 | */ 76 | 77 | /* 78 | tileAttribute("statusText", key: "SLIDER_CONTROL") { 79 | attributeState "level", action:"switch level.setLevel" 80 | } 81 | */ 82 | 83 | } 84 | 85 | 86 | 87 | valueTile("frontTile", "device.temperature", width: 2, height: 2) { 88 | state("temperature", label:'${currentValue}°', backgroundColor:"#e8e3d8") 89 | } 90 | 91 | valueTile("temperature", "device.temperature", width: 2, height: 2) { 92 | state("temperature", label:'${currentValue}°', backgroundColor:"#0A1E2C") 93 | } 94 | 95 | standardTile("fanMode", "device.thermostatFanMode", decoration: "flat", width: 2, height: 2) { 96 | state "fanAuto", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-auto" 97 | state "fanOn", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-on" 98 | } 99 | 100 | 101 | standardTile("mode", "device.thermostatMode", decoration: "flat", width: 2, height: 2) { 102 | state "off", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.heating-cooling-off", nextState:"heating" 103 | state "heat", action:"setThermostatMode", backgroundColor:"#ff6e7e", icon:"st.thermostat.heat", nextState:"cooling" 104 | state "cool", action:"setThermostatMode", backgroundColor:"#90d0e8", icon:"st.thermostat.cool", nextState:"..." 105 | //state "auto", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.auto" 106 | state "heating", action:"setThermostatMode", nextState:"to_cool" 107 | state "cooling", action:"setThermostatMode", nextState:"..." 108 | state "...", action:"off", nextState:"off" 109 | } 110 | 111 | valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 2, height: 2) { 112 | state "off", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" 113 | state "heat", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" 114 | state "cool", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" 115 | } 116 | valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, width: 2, height: 2) { 117 | state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" 118 | } 119 | valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, width: 2, height: 2) { 120 | state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" 121 | } 122 | standardTile("thermostatOperatingState", "device.thermostatOperatingState", inactiveLabel: false, width: 2, height: 2) { 123 | state "heating", backgroundColor:"#ff6e7e" 124 | state "cooling", backgroundColor:"#90d0e8" 125 | state "fan only", backgroundColor:"#e8e3d8" 126 | } 127 | standardTile("setpointUp", "device.thermostatSetpoint", decoration: "flat", width: 3, height:1) { 128 | state "setpointUp", action:"setpointUp", icon:"st.thermostat.thermostat-up", backgroundColor:"#0a1ec2" 129 | } 130 | 131 | standardTile("setpointDown", "device.thermostatSetpoint", decoration: "flat", width: 3, height:1) { 132 | state "setpointDown", action:"setpointDown", icon:"st.thermostat.thermostat-down", backgroundColor:"#0a1ec2" 133 | } 134 | 135 | standardTile("refresh", "device.temperature", decoration: "flat", width: 2, height: 2) { 136 | state "default", action:"refresh.refresh", icon:"st.secondary.refresh" 137 | } 138 | 139 | standardTile("configure", "device.configure", decoration: "flat", width: 2, height: 2) { 140 | state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" 141 | } 142 | 143 | main "richtstat" 144 | details(["richtstat", "setpointUp", "setpointDown", "temperature", "fanMode", "mode", "thermostatSetpoint", "refresh", "configure"]) 145 | } 146 | } 147 | 148 | 149 | // parse events into attributes 150 | def parse(String description) { 151 | log.debug "Parse description $description" 152 | def map = [:] 153 | def activeSetpoint = "--" 154 | def statusTextmsg = "" 155 | statusTextmsg = "Currently ${device.currentState('thermostatOperatingState').value} and set at ${device.currentState('thermostatSetpoint').value} and the fan is in ${device.currentState('thermostatFanMode').value} mode" 156 | //statusTextmsg = "This is a static message" 157 | sendEvent("name":"statusText", "value":statusTextmsg) 158 | def tstatLevel = device.currentState('thermostatSetpoint').value 159 | sendEvent("name":"level", "value":tstatLevel) 160 | if (description?.startsWith("read attr -")) 161 | { 162 | def descMap = parseDescriptionAsMap(description) 163 | // Thermostat Cluster Attribute Read Response 164 | if (descMap.cluster == "0201" && descMap.attrId == "0000") 165 | { 166 | log.debug "LOCAL TEMPERATURE" 167 | map.name = "temperature" 168 | map.value = getTemperature(descMap.value) 169 | def receivedTemperature = map.value 170 | } 171 | else if (descMap.cluster == "0201" && descMap.attrId == "001c") 172 | { 173 | map.name = "thermostatMode" 174 | map.value = getModeMap()[descMap.value] 175 | if (map.value == "cool") { 176 | activeSetpoint = device.currentValue("coolingSetpoint") 177 | } else if (map.value == "heat") { 178 | activeSetpoint = device.currentValue("heatingSetpoint") 179 | } 180 | sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) 181 | } 182 | else if (descMap.cluster == "0201" && descMap.attrId == "0011") 183 | { 184 | log.debug "COOL SET POINT" 185 | map.name = "coolingSetpoint" 186 | map.value = getTemperature(descMap.value) 187 | if (device.currentState("thermostatMode")?.value == "cool") { 188 | activeSetpoint = map.value 189 | log.debug "Active set point value: $activeSetpoint" 190 | sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) 191 | } 192 | } 193 | else if (descMap.cluster == "0201" && descMap.attrId == "0012") 194 | { 195 | log.debug "HEAT SET POINT" 196 | map.name = "heatingSetpoint" 197 | map.value = getTemperature(descMap.value) 198 | if (device.currentState("thermostatMode")?.value == "heat") { 199 | activeSetpoint = map.value 200 | sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) 201 | } 202 | } 203 | else if (descMap.cluster == "0201" && descMap.attrId == "0029") 204 | { 205 | log.debug "OPERATING STATE" 206 | map.name = "thermostatOperatingState" 207 | map.value = getOperatingStateMap()[descMap.value] 208 | } 209 | 210 | // Fan Control Cluster Attribute Read Response 211 | else if (descMap.cluster == "0202" && descMap.attrId == "0000") 212 | { 213 | map.name = "thermostatFanMode" 214 | map.value = getFanModeMap()[descMap.value] 215 | } 216 | 217 | }// End of Read Attribute Response 218 | 219 | /*else if (description?.startsWith("updated")) { 220 | configure() 221 | } 222 | */ 223 | def result = null 224 | if (map) { 225 | result = createEvent(map) 226 | } 227 | log.debug "Parse returned $map" 228 | 229 | return result 230 | } 231 | 232 | // =============== Help Functions - Don't use log.debug in all these functins =============== 233 | def parseDescriptionAsMap(description) { 234 | (description - "read attr - ").split(",").inject([:]) { map, param -> 235 | def nameAndValue = param.split(":") 236 | map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] 237 | } 238 | } 239 | 240 | def getModeMap() { [ 241 | "00":"off", 242 | "03":"cool", 243 | "04":"heat" 244 | ]} 245 | 246 | def getOperatingStateMap() { [ 247 | "0000":"idle", 248 | "0001":"heating", 249 | "0002":"cooling", 250 | "0004":"fan only", 251 | "0005":"heating", 252 | "0006":"cooling", 253 | "0008":"heating", 254 | "0009":"heating", 255 | "000A":"heating", 256 | "000D":"heating", 257 | "0010":"cooling", 258 | "0012":"cooling", 259 | "0014":"cooling", 260 | "0015":"cooling" 261 | ]} 262 | 263 | def getFanModeMap() { [ 264 | "04":"fanOn", 265 | "05":"fanAuto" 266 | ]} 267 | 268 | def getTemperatureDisplayModeMap() { [ 269 | "00":"C", 270 | "01":"F" 271 | ]} 272 | 273 | 274 | def getTemperature(value) 275 | { 276 | def decimalFormat = new java.text.DecimalFormat("#") 277 | def celsius = Integer.parseInt(value, 16) / 100.0 as Double 278 | def returnValue 279 | 280 | // Format to support decimal with one or two 281 | decimalFormat.setMaximumFractionDigits(2) 282 | decimalFormat.setMinimumFractionDigits(1) 283 | 284 | returnValue = decimalFormat.format(celsius); 285 | 286 | log.debug "Temperature value in C: $returnValue" 287 | 288 | if(getTemperatureScale() == "F"){ 289 | returnValue = decimalFormat.format(Math.round(celsiusToFahrenheit(celsius)*10)/10.0) 290 | 291 | log.debug "Temperature value in F: $returnValue" 292 | } 293 | 294 | return returnValue 295 | } 296 | 297 | 298 | 299 | // =============== Setpoints =============== 300 | def setpointUp() 301 | { 302 | def currentMode = device.currentState("thermostatMode")?.value 303 | def currentUnit = getTemperatureScale() 304 | 305 | // check if heating or cooling setpoint needs to be changed 306 | double nextLevel = device.currentValue("thermostatSetpoint") + 1.0 307 | log.debug "Next level: $nextLevel" 308 | 309 | // check the limits 310 | if(currentUnit == "C") 311 | { 312 | if (currentMode == "cool") 313 | { 314 | if(nextLevel > 36.0) 315 | { 316 | nextLevel = 36.0 317 | } 318 | } else if (currentMode == "heat") 319 | { 320 | if(nextLevel > 32.0) 321 | { 322 | nextLevel = 32.0 323 | } 324 | } 325 | } 326 | else //in degF unit 327 | { 328 | if (currentMode == "cool") 329 | { 330 | if(nextLevel > 96.0) 331 | { 332 | nextLevel = 96.0 333 | } 334 | } else if (currentMode == "heat") 335 | { 336 | if(nextLevel > 89.0) 337 | { 338 | nextLevel = 89.0 339 | } 340 | } 341 | } 342 | 343 | log.debug "setpointUp() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}" 344 | 345 | setSetpoint(nextLevel) 346 | } 347 | 348 | def setpointDown() 349 | { 350 | def currentMode = device.currentState("thermostatMode")?.value 351 | def currentUnit = getTemperatureScale() 352 | 353 | // check if heating or cooling setpoint needs to be changed 354 | double nextLevel = device.currentValue("thermostatSetpoint") - 1.0 355 | 356 | // check the limits 357 | if (currentUnit == "C") 358 | { 359 | if (currentMode == "cool") 360 | { 361 | if(nextLevel < 8.0) 362 | { 363 | nextLevel = 8.0 364 | } 365 | } else if (currentMode == "heat") 366 | { 367 | if(nextLevel < 10.0) 368 | { 369 | nextLevel = 10.0 370 | } 371 | } 372 | } 373 | else //in degF unit 374 | { 375 | if (currentMode == "cool") 376 | { 377 | if (nextLevel < 47.0) 378 | { 379 | nextLevel = 47.0 380 | } 381 | } else if (currentMode == "heat") 382 | { 383 | if (nextLevel < 50.0) 384 | { 385 | nextLevel = 50.0 386 | } 387 | } 388 | } 389 | 390 | log.debug "setpointDown() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}" 391 | 392 | setSetpoint(nextLevel) 393 | } 394 | 395 | 396 | def setSetpoint(degrees) 397 | { 398 | def temperatureScale = getTemperatureScale() 399 | def currentMode = device.currentState("thermostatMode")?.value 400 | 401 | def degreesDouble = degrees as Double 402 | sendEvent("name":"thermostatSetpoint", "value":degreesDouble) 403 | log.debug "New set point: $degreesDouble" 404 | 405 | def celsius = (getTemperatureScale() == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) 406 | if (currentMode == "cool") { 407 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}" 408 | } 409 | else if (currentMode == "heat") { 410 | 411 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}" 412 | } 413 | } 414 | 415 | def setHeatingSetpoint(degrees) { 416 | def temperatureScale = getTemperatureScale() 417 | 418 | def degreesDouble = degrees as Double 419 | log.debug "setHeatingSetpoint({$degreesDouble} ${temperatureScale})" 420 | sendEvent("name":"heatingSetpoint", "value":degreesDouble) 421 | 422 | def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) 423 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}" 424 | } 425 | 426 | def setCoolingSetpoint(degrees) { 427 | def temperatureScale = getTemperatureScale() 428 | 429 | def degreesDouble = degrees as Double 430 | log.debug "setCoolingSetpoint({$degreesDouble} ${temperatureScale})" 431 | sendEvent("name":"coolingSetpoint", "value":degreesDouble) 432 | 433 | def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) 434 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}" 435 | } 436 | 437 | // =============== Thermostat Mode =============== 438 | def modes() { 439 | ["off", "heat", "cool"] 440 | } 441 | 442 | def setThermostatMode() 443 | { 444 | def currentMode = device.currentState("thermostatMode")?.value 445 | def modeOrder = modes() 446 | def index = modeOrder.indexOf(currentMode) 447 | def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0] 448 | log.debug "setThermostatMode - switching from $currentMode to $next" 449 | "$next"() 450 | } 451 | 452 | def setThermostatMode(String value) { 453 | "$value"() 454 | } 455 | 456 | def off() { 457 | sendEvent("name":"thermostatMode", "value":"off") 458 | sendEvent("name":"thermostatSetpoint","value":"--") 459 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}" 460 | } 461 | 462 | def cool() { 463 | def coolingSetpoint = device.currentValue("coolingSetpoint") 464 | log.debug "Cool set point: $coolingSetpoint" 465 | sendEvent("name":"thermostatMode", "value":"cool") 466 | sendEvent("name":"thermostatSetpoint","value":coolingSetpoint) 467 | [ 468 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {03}" 469 | ] 470 | } 471 | 472 | def heat() { 473 | def heatingSetpoint = device.currentValue("heatingSetpoint") 474 | log.debug "Heat set point: $heatingSetpoint" 475 | sendEvent("name":"thermostatMode","value":"heat") 476 | sendEvent("name":"thermostatSetpoint","value":heatingSetpoint) 477 | [ 478 | "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}" 479 | ] 480 | } 481 | 482 | // =============== Fan Mode =============== 483 | def setThermostatFanMode() 484 | { 485 | def currentFanMode = device.currentState("thermostatFanMode")?.value 486 | def returnCommand 487 | 488 | switch (currentFanMode) { 489 | case "fanAuto": 490 | returnCommand = fanOn() 491 | break 492 | case "fanOn": 493 | returnCommand = fanAuto() 494 | break 495 | } 496 | 497 | if(!currentFanMode) { 498 | returnCommand = fanAuto() 499 | } 500 | 501 | log.debug "setThermostatFanMode - switching from $currentFanMode to $returnCommand" 502 | 503 | returnCommand 504 | } 505 | 506 | def setThermostatFanMode(String value) { 507 | "$value"() 508 | } 509 | 510 | def on() { 511 | fanOn() 512 | } 513 | 514 | def fanOn() { 515 | sendEvent("name":"thermostatFanMode", "value":"fanOn") 516 | "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}" 517 | } 518 | 519 | def auto() { 520 | fanAuto() 521 | } 522 | 523 | def fanAuto() { 524 | sendEvent("name":"thermostatFanMode", "value":"fanAuto") 525 | "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}" 526 | } 527 | 528 | 529 | 530 | 531 | // =============== SmartThings Default Fucntions: refresh, configure, poll =============== 532 | def refresh() 533 | { 534 | log.debug "refresh() - update attributes " 535 | [ 536 | 537 | //Set long poll interval to 2 qs 538 | "raw 0x0020 {11 00 02 02 00 00 00}", 539 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 540 | 541 | //This is sent in this specific order to ensure that the temperature values are received after the unit/mode 542 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 800", 543 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 800", 544 | 545 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x11", "delay 800", 546 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 800", 547 | "st rattr 0x${device.deviceNetworkId} 1 0x201 0x29", "delay 800", 548 | "st rattr 0x${device.deviceNetworkId} 1 0x202 0", "delay 800", 549 | 550 | //Set long poll interval to 28 qs (7 seconds) 551 | "raw 0x0020 {11 00 02 1C 00 00 00}", 552 | "send 0x${device.deviceNetworkId} 1 1" 553 | ] 554 | } 555 | 556 | def poll() 557 | { 558 | refresh() 559 | } 560 | 561 | def configure() 562 | { 563 | log.debug "configure() - binding & attribute report" 564 | [ 565 | //Set long poll interval to 2 qs 566 | "raw 0x0020 {11 00 02 02 00 00 00}", 567 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 568 | 569 | //Thermostat - Cluster 201 570 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500", 571 | 572 | "zcl global send-me-a-report 0x201 0 0x29 5 300 {3200}", 573 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 574 | 575 | "zcl global send-me-a-report 0x201 0x0011 0x29 5 300 {3200}", 576 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 577 | 578 | "zcl global send-me-a-report 0x201 0x0012 0x29 5 300 {3200}", 579 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 580 | 581 | "zcl global send-me-a-report 0x201 0x001C 0x30 5 300 {}", 582 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 583 | 584 | "zcl global send-me-a-report 0x201 0x0029 0x19 5 300 {}", 585 | "send 0x${device.deviceNetworkId} 1 1", "delay 500", 586 | 587 | //Fan Control - Cluster 202 588 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}", "delay 500", 589 | 590 | "zcl global send-me-a-report 0x202 0 0x30 5 300 {}", 591 | "send 0x${device.deviceNetworkId} 1 1", "delay 1500", 592 | 593 | ] + refresh() 594 | } 595 | 596 | 597 | 598 | private hex(value) { 599 | new BigInteger(Math.round(value).toString()).toString(16) 600 | } 601 | 602 | def setLevel(value) { 603 | log.debug "Selected Level is $value" 604 | def privMin = 55 605 | def privMax = 85 606 | def newTempF = (((value.toInteger() - 1) * (privMax.toInteger() - privMin.toInteger()) / (100/1) + privMin.toInteger())) 607 | log.debug "Adjusted Range value is $newTempF" 608 | setSetpoint(newTempF) 609 | } --------------------------------------------------------------------------------