├── README.md ├── Switches.groovy ├── HelloHomeBridge.goovy ├── JSON.groovy └── loft.groovy /README.md: -------------------------------------------------------------------------------- 1 | # SmartThings 2 | 3 | ## Dev Resources 4 | 5 | * https://graph.api.smartthings.com/ide/doc/smartApp 6 | * https://groovyconsole.appspot.com/ 7 | * http://community.smartthings.com/t/any-documentation-on-physicalgraph-device-hubaction/3117/17 8 | 9 | ## My resources 10 | 11 | * [Logs](https://papertrailapp.com/systems/jnewland-smartthings-logs/events) 12 | -------------------------------------------------------------------------------- /Switches.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Jesse Newland 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 | */ 14 | definition( 15 | name: "Hello Home / Mode Switches", 16 | namespace: "jnewland", 17 | author: "Jesse Newland", 18 | description: "Name on/off tiles the same as your Hello Home phrases or Modes", 19 | category: "SmartThings Labs", 20 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 21 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 22 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 23 | 24 | 25 | def installed() { 26 | initialize() 27 | } 28 | 29 | def updated() { 30 | unsubscribe() 31 | initialize() 32 | } 33 | 34 | def initialize() { 35 | subscribe(switches, "switch", switchHandler) 36 | } 37 | 38 | def switchHandler(evt) { 39 | def s = switches.find{ evt.deviceId == it.id } 40 | def phrase = location.helloHome.getPhrases().find { it.label == s.displayName } 41 | if (phrase) { 42 | location.helloHome.execute(phrase.label) 43 | } 44 | def mode = location.modes.find { it.name == s.displayName } 45 | if (mode) { 46 | setLocationMode(mode) 47 | } 48 | } 49 | 50 | preferences { 51 | page(name: selectSwitches) 52 | } 53 | 54 | def selectSwitches() { 55 | dynamicPage(name: "selectSwitches", title: "Switches", install: true) { 56 | section("Select switches named after Hello Home phrases") { 57 | input "switches", "capability.switch", title: "Switches", multiple: true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /HelloHomeBridge.goovy: -------------------------------------------------------------------------------- 1 | /** 2 | * HelloHomeBridge 3 | * 4 | * Copyright 2015 Jesse Newland 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: "Hello HomeBridge", 18 | namespace: "jnewland", 19 | author: "Jesse Newland", 20 | description: "A SmartThings app designed to work with https://github.com/jnewland/homebridge to provide Siri control for your HelloHome actions.", 21 | category: "SmartThings Labs", 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 | def installed() { 29 | log.debug "Installed with settings: ${settings}" 30 | initialize() 31 | } 32 | 33 | def updated() { 34 | log.debug "Updated with settings: ${settings}" 35 | unsubscribe() 36 | initialize() 37 | } 38 | 39 | def initialize() { 40 | if (!state.accessToken) { 41 | createAccessToken() 42 | } 43 | } 44 | 45 | preferences { 46 | page(name: "copyConfig") 47 | } 48 | 49 | def copyConfig() { 50 | dynamicPage(name: "copyConfig", title: "Config", install:true) { 51 | section() { 52 | paragraph "Copy/Paste the below into your homebridge's config.json to create HomeKit accessories for your Hello Home actions" 53 | href url:"https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\"" 54 | } 55 | } 56 | } 57 | 58 | def renderConfig() { 59 | def configJson = new groovy.json.JsonOutput().toJson(location?.helloHome?.getPhrases().collect({ 60 | [ 61 | accessory: "SmartThingsHelloHome", 62 | name: it.label, 63 | appId: it.id, 64 | accessToken: state.accessToken 65 | ] 66 | })) 67 | 68 | def configString = new groovy.json.JsonOutput().prettyPrint(configJson) 69 | render contentType: "text/plain", data: configString 70 | } 71 | 72 | mappings { 73 | if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) { 74 | path("/config") { action: [GET: "authError"] } 75 | } else { 76 | path("/config") { action: [GET: "renderConfig"] } 77 | } 78 | } 79 | 80 | def authError() { 81 | [error: "Permission denied"] 82 | } 83 | -------------------------------------------------------------------------------- /JSON.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON 3 | * 4 | * Copyright 2015 Jesse Newland 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: "JSON API", 18 | namespace: "jnewland", 19 | author: "Jesse Newland", 20 | description: "A JSON API for SmartThings", 21 | category: "SmartThings Labs", 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 | def installed() { 29 | initialize() 30 | } 31 | 32 | def updated() { 33 | unsubscribe() 34 | initialize() 35 | } 36 | 37 | def initialize() { 38 | if (!state.accessToken) { 39 | createAccessToken() 40 | } 41 | } 42 | 43 | preferences { 44 | page(name: "copyConfig") 45 | } 46 | 47 | def copyConfig() { 48 | if (!state.accessToken) { 49 | createAccessToken() 50 | } 51 | dynamicPage(name: "copyConfig", title: "Config", install:true) { 52 | section("Select devices to include in the /devices API call") { 53 | input "switches", "capability.switch", title: "Switches", multiple: true, required: false 54 | input "hues", "capability.colorControl", title: "Hues", multiple: true, required: false 55 | } 56 | 57 | section() { 58 | paragraph "View this SmartApp's configuration to use it in other places." 59 | href url:"https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\"" 60 | } 61 | 62 | section() { 63 | href url:"https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Debug", description:"View accessories JSON" 64 | } 65 | } 66 | } 67 | 68 | def renderConfig() { 69 | def configJson = new groovy.json.JsonOutput().toJson([ 70 | description: "JSON API", 71 | platforms: [ 72 | [ 73 | platform: "SmartThings", 74 | name: "SmartThings", 75 | app_id: app.id, 76 | access_token: state.accessToken 77 | ] 78 | ], 79 | ]) 80 | 81 | def configString = new groovy.json.JsonOutput().prettyPrint(configJson) 82 | render contentType: "text/plain", data: configString 83 | } 84 | 85 | def deviceCommandMap(device, type) { 86 | device.supportedCommands.collectEntries { command-> 87 | def commandUrl = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/${type}/${device.id}/command/${command.name}?access_token=${state.accessToken}" 88 | [ 89 | (command.name): commandUrl 90 | ] 91 | } 92 | } 93 | 94 | def authorizedDevices() { 95 | [ 96 | switches: switches, 97 | hues: hues 98 | ] 99 | } 100 | 101 | def renderDevices() { 102 | def deviceData = authorizedDevices().collectEntries { devices-> 103 | [ 104 | (devices.key): devices.value.collect { device-> 105 | [ 106 | name: device.displayName, 107 | commands: deviceCommandMap(device, devices.key) 108 | ] 109 | } 110 | ] 111 | } 112 | def deviceJson = new groovy.json.JsonOutput().toJson(deviceData) 113 | def deviceString = new groovy.json.JsonOutput().prettyPrint(deviceJson) 114 | render contentType: "application/json", data: deviceString 115 | } 116 | 117 | def deviceCommand() { 118 | def device = authorizedDevices()[params.type].find { it.id == params.id } 119 | def command = params.command 120 | if (!device) { 121 | httpError(404, "Device not found") 122 | } else { 123 | if (params.value) { 124 | device."$command"(params.value) 125 | } else { 126 | device."$command"() 127 | } 128 | } 129 | } 130 | 131 | mappings { 132 | if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) { 133 | path("/devices") { action: [GET: "authError"] } 134 | path("/config") { action: [GET: "authError"] } 135 | path("/:type/:id/command/:command") { action: [PUT: "authError"] } 136 | } else { 137 | path("/devices") { action: [GET: "renderDevices"] } 138 | path("/config") { action: [GET: "renderConfig"] } 139 | path("/:type/:id/command/:command") { action: [PUT: "deviceCommand"] } 140 | } 141 | } 142 | 143 | def authError() { 144 | [error: "Permission denied"] 145 | } 146 | -------------------------------------------------------------------------------- /loft.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Jesse Newland 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 | */ 14 | definition( 15 | name: "Loft", 16 | namespace: "jnewland", 17 | author: "Jesse Newland", 18 | description: "All the business logic for my crappy loft lives here", 19 | category: "SmartThings Labs", 20 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 21 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 22 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 23 | 24 | preferences { 25 | page(name: selectThings) 26 | } 27 | 28 | def selectThings() { 29 | dynamicPage(name: "selectThings", title: "Select Things", install: true) { 30 | section("Webhook URL"){ 31 | input "url", "text", title: "Webhook URL", description: "Your webhook URL", required: true 32 | } 33 | section("Things to monitor for events") { 34 | input "monitor_switches", "capability.switch", title: "Switches", multiple: true, required: false 35 | input "monitor_motion", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false 36 | input "monitor_presence", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false 37 | } 38 | section("Things to control") { 39 | input "hues", "capability.colorControl", title: "Hue Bulbs", multiple: true, required: false 40 | input "switches", "capability.switch", title: "Switches", multiple: true, required: false 41 | input "dimmers", "capability.switchLevel", title: "Dimmers", multiple: true, required: false 42 | } 43 | section("Voice") { 44 | input "voice", "capability.speechSynthesis", title: "Voice", required: false 45 | } 46 | } 47 | } 48 | 49 | 50 | def installed() { 51 | initialize() 52 | } 53 | 54 | def updated() { 55 | unsubscribe() 56 | unschedule() 57 | initialize() 58 | } 59 | 60 | def initialize() { 61 | subscribe(monitor_switches, "switch", eventHandler) 62 | subscribe(monitor_motion, "motion", eventHandler) 63 | subscribe(monitor_presence, "presence", eventHandler) 64 | subscribe(location, "mode", eventHandler) 65 | subscribe(location, "sunset", eventHandler) 66 | subscribe(location, "sunrise", eventHandler) 67 | tick() 68 | } 69 | 70 | def eventHandler(evt) { 71 | def everyone_here = presense_is_after(monitor_presence, "present", 10) 72 | def everyone_gone = presense_is_after(monitor_presence, "not present", 10) 73 | def current_count = monitor_presence.findAll { it.currentPresence == "present" }.size() 74 | webhook([ 75 | displayName: evt.displayName, 76 | value: evt.value, 77 | daytime: is_daytime(), 78 | mode: location.mode, 79 | current_count: current_count, 80 | everyone_here: everyone_here, 81 | everyone_gone: everyone_gone 82 | ]) 83 | 84 | if (mode == "Pause") { 85 | webhook([ at: 'paused' ]) 86 | log.info("No actions taken in Pause mode") 87 | } else { 88 | def lights = [switches, hues].flatten() 89 | // turn on lights near stairs when motion is detected 90 | if (evt.displayName == "motion@stairs" && evt.value == "active") { 91 | webhook([ at: 'stair_motion_lights' ]) 92 | lights.findAll { s -> 93 | s.displayName == "stairs" || 94 | s.displayName == "loft" || 95 | s.displayName == "entry" 96 | }.findAll { s -> 97 | s.currentSwitch == "off" 98 | }.each { s -> 99 | if ("setLevel" in s.supportedCommands.collect { it.name }) { 100 | if (location.mode == "Sleep") { 101 | s.setLevel(50) 102 | } else if (location.mode == "Home / Night") { 103 | s.setLevel(75) 104 | } else { 105 | s.setLevel(100) 106 | } 107 | } 108 | s.on() 109 | } 110 | } 111 | 112 | // Turn on some lights if one of us is up 113 | if (evt.value == "Yawn") { 114 | webhook([ at: 'yawn' ]) 115 | lights.findAll { s -> 116 | s.displayName == "loft" || 117 | s.displayName == "entry" || 118 | s.displayName == "chandelier" 119 | }.findAll { s -> 120 | s.currentSwitch == "off" 121 | }.each { s -> 122 | if ("setLevel" in s.supportedCommands.collect { it.name }) { 123 | s.setLevel(75) 124 | } 125 | s.on() 126 | } 127 | } 128 | 129 | // turn on all lights when entering day mode 130 | if (evt.value == "Home / Day") { 131 | webhook([ at: 'home_day' ]) 132 | lights.each { s -> 133 | if (s.currentSwitch == "off") { 134 | s.on() 135 | } 136 | if ("setLevel" in s.supportedCommands.collect { it.name }) { 137 | s.setLevel(100) 138 | } 139 | } 140 | } 141 | 142 | // turn on night mode at sunset 143 | if (evt.displayName == "sunset" && current_count > 0 && location.mode == "Home / Day") { 144 | webhook([ at: 'sunset' ]) 145 | changeMode("Home / Night") 146 | } 147 | 148 | // dim lights at night 149 | if (evt.value == "Home / Night") { 150 | webhook([ at: 'home_night' ]) 151 | lights.findAll { s -> 152 | "setLevel" in s.supportedCommands.collect { it.name } 153 | }.each { s -> 154 | log.info("Night mode enabled, dimming ${s.displayName}") 155 | s.setLevel(75) 156 | } 157 | 158 | // turn off that light by the door downstairs entirely 159 | lights.findAll { s -> 160 | s.displayName == "downstairs door" 161 | }.each { s -> 162 | log.info("Night mode enabled, turning off ${s.displayName}") 163 | s.off() 164 | } 165 | } 166 | 167 | // turn off all lights when entering sleep mode 168 | if (evt.value == "Sleep") { 169 | webhook([ at: 'sleep' ]) 170 | lights.findAll { s -> 171 | s.currentSwitch == "on" 172 | }.each { s -> 173 | log.info("Sleep mode enabled, turning off ${s.displayName}") 174 | s.off() 175 | } 176 | } 177 | 178 | // turn off all lights when everyone goes away 179 | if (everyone_gone && location.mode != "Away") { 180 | webhook([ at: 'away' ]) 181 | changeMode("Away") 182 | lights.findAll { s -> 183 | s.currentSwitch == "on" 184 | }.each { s -> 185 | log.info("Away mode enabled, turning off ${s.displayName}") 186 | s.off() 187 | } 188 | } 189 | 190 | // switch mode to Home when we return 191 | if (current_count > 0 && location.mode == "Away") { 192 | webhook([ at: 'home' ]) 193 | changeMode("Home") 194 | } 195 | 196 | // Make home mode specific based on day / night 197 | if (evt.value == "Home") { 198 | webhook([ at: 'home_day_night' ]) 199 | if (is_daytime()) { 200 | changeMode("Home / Day") 201 | } else { 202 | changeMode("Home / Night") 203 | } 204 | } 205 | 206 | if (canSchedule()) { 207 | runIn(61, tick) 208 | } else { 209 | webhook([ can_schedule: 'false' ]) 210 | log.error("can_schedule=false") 211 | } 212 | } 213 | } 214 | 215 | def changeMode(mode) { 216 | //voice?.speak("changing mode to ${mode}") 217 | setLocationMode(mode) 218 | eventHandler([ 219 | displayName: "changeMode", 220 | value: mode 221 | ]) 222 | } 223 | 224 | def tick() { 225 | eventHandler([ 226 | displayName: "tick", 227 | value: "tock" 228 | ]) 229 | } 230 | 231 | def webhook(map) { 232 | def successClosure = { response -> 233 | log.debug "Request was successful, $response" 234 | } 235 | 236 | def json_params = [ 237 | uri: settings.url, 238 | success: successClosure, 239 | body: map 240 | ] 241 | httpPostJson(json_params) 242 | } 243 | 244 | 245 | private is_daytime() { 246 | def data = getWeatherFeature("astronomy") 247 | def sunset = "${data.moon_phase.sunset.hour}${data.moon_phase.sunset.minute}" 248 | def sunrise = "${data.moon_phase.sunrise.hour}${data.moon_phase.sunrise.minute}" 249 | def current = "${data.moon_phase.current_time.hour}${data.moon_phase.current_time.minute}" 250 | if (current.toInteger() > sunrise.toInteger() && current.toInteger() < sunset.toInteger()) { 251 | return true 252 | } 253 | else { 254 | return false 255 | } 256 | } 257 | 258 | private presense_is_after(people, presence, minutes) { 259 | def result = true 260 | for (person in people) { 261 | if (person.currentPresence != presence) { 262 | result = false 263 | break 264 | } else { 265 | def threshold = 1000 * 60 * minutes 266 | def elapsed = now() - person.currentState("presence").rawDateCreated.time 267 | if (elapsed < threshold) { 268 | result = false 269 | break 270 | } 271 | } 272 | } 273 | return result 274 | } 275 | --------------------------------------------------------------------------------