Update step-notifier.groovy
[smartapps.git] / official / wattvision-manager.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  *  Wattvision Manager
14  *
15  *  Author: steve
16  *  Date: 2014-02-13
17  */
18
19 // Automatically generated. Make future change here.
20 definition(
21         name: "Wattvision Manager",
22         namespace: "smartthings",
23         author: "SmartThings",
24         description: "Monitor your whole-house energy use by connecting to your Wattvision account",
25         iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision.png",
26         iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision%402x.png",
27         oauth: [displayName: "Wattvision", displayLink: "https://www.wattvision.com/"]
28 )
29
30 preferences {
31         page(name: "rootPage")
32 }
33
34 def rootPage() {
35         def sensors = state.sensors
36         def hrefState = sensors ? "complete" : ""
37         def hrefDescription = ""
38         sensors.each { sensorId, sensorName ->
39                 hrefDescription += "${sensorName}\n"
40         }
41
42         dynamicPage(name: "rootPage", install: sensors ? true : false, uninstall: true) {
43                 section {
44                         href(url: loginURL(), title: "Connect Wattvision Sensors", style: "embedded", description: hrefDescription, state: hrefState)
45                 }
46                 section {
47                         href(url: "https://www.wattvision.com", title: "Learn More About Wattvision", style: "external", description: null)
48                 }
49         }
50 }
51
52 mappings {
53         path("/access") {
54                 actions:
55                 [
56                         POST  : "setApiAccess",
57                         DELETE: "revokeApiAccess"
58                 ]
59         }
60         path("/devices") {
61                 actions:
62                 [
63                         GET: "listDevices"
64                 ]
65         }
66         path("/device/:sensorId") {
67                 actions:
68                 [
69                         GET   : "getDevice",
70                         PUT   : "updateDevice",
71                         POST  : "createDevice",
72                         DELETE: "deleteDevice"
73                 ]
74         }
75         path("/${loginCallbackPath()}") {
76                 actions:
77                 [
78                         GET: "loginCallback"
79                 ]
80         }
81 }
82
83 def installed() {
84         log.debug "Installed with settings: ${settings}"
85
86         initialize()
87 }
88
89 def updated() {
90         log.debug "Updated with settings: ${settings}"
91
92         unsubscribe()
93         unschedule()
94         initialize()
95 }
96
97 def initialize() {
98         getDataFromWattvision()
99         scheduleDataCollection()
100 }
101
102 def getDataFromWattvision() {
103
104         log.trace "Getting data from Wattvision"
105
106         def children = getChildDevices()
107         if (!children) {
108                 log.warn "No children. Not collecting data from Wattviwion"
109                 // currently only support one child
110                 return
111         }
112
113         def endDate = new Date()
114         def startDate
115
116         if (!state.lastUpdated) {
117 //              log.debug "no state.lastUpdated"
118                 startDate = new Date(hours: endDate.hours - 3)
119         } else {
120 //              log.debug "parsing state.lastUpdated"
121                 startDate = new Date().parse(smartThingsDateFormat(), state.lastUpdated)
122         }
123
124         state.lastUpdated = endDate.format(smartThingsDateFormat())
125
126         children.each { child ->
127                 getDataForChild(child, startDate, endDate)
128         }
129
130 }
131
132 def getDataForChild(child, startDate, endDate) {
133         if (!child) {
134                 return
135         }
136
137         def wattvisionURL = wattvisionURL(child.deviceNetworkId, startDate, endDate)
138         if (wattvisionURL) {
139                 try {
140                         httpGet(uri: wattvisionURL) { response ->
141                                 def json = new org.json.JSONObject(response.data.toString())
142                                 child.addWattvisionData(json)
143                                 return "success"
144                         }
145                 } catch (groovyx.net.http.HttpResponseException httpE) {
146                         log.error "Wattvision getDataForChild HttpResponseException: ${httpE} -> ${httpE.response.data}"
147                         //log.debug "wattvisionURL = ${wattvisionURL}"
148                         return "fail"
149                 } catch (e) {
150                         log.error "Wattvision getDataForChild General Exception: ${e}"
151                         //log.debug "wattvisionURL = ${wattvisionURL}"
152                         return "fail"
153                 }
154         }
155 }
156
157 def wattvisionURL(senorId, startDate, endDate) {
158
159         log.trace "getting wattvisionURL"
160
161         def wattvisionApiAccess = state.wattvisionApiAccess
162         if (!wattvisionApiAccess.id || !wattvisionApiAccess.key) {
163                 return null
164         }
165
166         if (!endDate) {
167                 endDate = new Date()
168         }
169         if (!startDate) {
170                 startDate = new Date(hours: endDate.hours - 3)
171         }
172
173         def diff = endDate.getTime() - startDate.getTime()
174         if (diff > 259200000) { // 3 days in milliseconds
175                 // Wattvision only allows pulling 3 hours of data at a time
176                 startDate = new Date(hours: endDate.hours - 3)
177         } else if (diff < 10000) { // 10 seconds in milliseconds
178                 // Wattvision throws errors when the difference between start_time and end_time is 5 seconds or less
179                 // So we are going to make sure that we have a few more seconds of breathing room
180                 use (groovy.time.TimeCategory) {
181                         startDate = endDate - 10.seconds
182                 }
183         }
184
185         def params = [
186                 "sensor_id" : senorId,
187                 "api_id"    : wattvisionApiAccess.id,
188                 "api_key"   : wattvisionApiAccess.key,
189                 "type"      : wattvisionDataType ?: "rate",
190                 "start_time": startDate.format(wattvisionDateFormat()),
191                 "end_time"  : endDate.format(wattvisionDateFormat())
192         ]
193
194         def parameterString = params.collect { key, value -> "${key.encodeAsURL()}=${value.encodeAsURL()}" }.join("&")
195         def accessURL = wattvisionApiAccess.url ?: "https://www.wattvision.com/api/v0.2/elec"
196         def url = "${accessURL}?${parameterString}"
197
198 //      log.debug "wattvisionURL: ${url}"
199         return url
200 }
201
202 def getData() {
203         state.lastUpdated = new Date().format(smartThingsDateFormat())
204 }
205
206 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
207
208 public wattvisionDateFormat() { "yyyy-MM-dd'T'HH:mm:ss" }
209
210 def childMarshaller(child) {
211         return [
212                 name     : child.name,
213                 label    : child.label,
214                 sensor_id: child.deviceNetworkId,
215                 location : child.location.name
216         ]
217 }
218
219 // ========================================================
220 // ENDPOINTS
221 // ========================================================
222
223 def listDevices() {
224         getChildDevices().collect { childMarshaller(it) }
225 }
226
227 def getDevice() {
228
229         log.trace "Getting device"
230
231         def child = getChildDevice(params.sensorId)
232
233         if (!child) {
234                 httpError(404, "Device not found")
235         }
236
237         return childMarshaller(child)
238 }
239
240 def updateDevice() {
241
242         log.trace "Updating Device with data from Wattvision"
243
244         def body = request.JSON
245
246         def child = getChildDevice(params.sensorId)
247
248         if (!child) {
249                 httpError(404, "Device not found")
250         }
251
252         child.addWattvisionData(body)
253
254         render([status: 204, data: " "])
255 }
256
257 def createDevice() {
258
259         log.trace "Creating Wattvision device"
260
261         if (getChildDevice(params.sensorId)) {
262                 httpError(403, "Device already exists")
263         }
264
265         def child = addChildDevice("smartthings", "Wattvision", params.sensorId, null, [name: "Wattvision", label: request.JSON.label])
266
267         child.setGraphUrl(getGraphUrl(params.sensorId));
268
269         getDataForChild(child, null, null)
270
271         return childMarshaller(child)
272 }
273
274 def deleteDevice() {
275
276         log.trace "Deleting Wattvision device"
277
278         deleteChildDevice(params.sensorId)
279         render([status: 204, data: " "])
280 }
281
282 def setApiAccess() {
283
284         log.trace "Granting access to Wattvision API"
285
286         def body = request.JSON
287
288         state.wattvisionApiAccess = [
289                 url: body.url,
290                 id : body.id,
291                 key: body.key
292         ]
293
294         scheduleDataCollection()
295
296         render([status: 204, data: " "])
297 }
298
299 def scheduleDataCollection() {
300         schedule("* /1 * * * ?", "getDataFromWattvision") // every 1 minute
301 }
302
303 def revokeApiAccess() {
304
305         log.trace "Revoking access to Wattvision API"
306
307         state.wattvisionApiAccess = [:]
308         render([status: 204, data: " "])
309 }
310
311 public getGraphUrl(sensorId) {
312
313         log.trace "Collecting URL for Wattvision graph"
314
315         def apiId = state.wattvisionApiAccess.id
316         def apiKey = state.wattvisionApiAccess.key
317
318         // TODO: allow the changing of type?
319         "http://www.wattvision.com/partners/smartthings/charts?s=${sensorId}&api_id=${apiId}&api_key=${apiKey}&type=w"
320 }
321
322 // ========================================================
323 // SmartThings initiated setup
324 // ========================================================
325
326 /* Debug info for Steve / Andrew
327
328 this page: /partners/smartthings/whatswv
329         - linked from within smartthings, will tell you how to get a wattvision sensor, etc.
330         - pass the debug flag (?debug=1) to show this text.
331
332 login page: /partners/smartthings/login?callback_url=CALLBACKURL
333         - open this page, which will require login.
334         - once login is complete, we call you back at callback_url with:
335                 <callback_url>?id=<wattvision_api_id>&key=<wattvision_api_key>
336                         question: will you know which user this is on your end?
337
338 sensor json: /partners/smartthings/sensor_list?api_id=...&api_key=...
339         - returns a list of sensors and their associated house names, as a json object
340         - example return value with one sensor id 2, associated with house 'Test's House'
341                 - content type is application/json
342                 - {"2": "Test's House"}
343
344 */
345
346 def loginCallback() {
347         log.trace "loginCallback"
348
349         state.wattvisionApiAccess = [
350                 id : params.id,
351                 key: params.key
352         ]
353
354         getSensorJSON(params.id, params.key)
355
356         connectionSuccessful("Wattvision", "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision@2x.png")
357 }
358
359 private getSensorJSON(id, key) {
360         log.trace "getSensorJSON"
361
362         def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
363
364     httpGet(uri: sensorUrl) { response ->
365
366                 def sensors = [:]
367
368         response.data.each { sensorId, sensorName ->
369                 sensors[sensorId] = sensorName
370                         createChild(sensorId, sensorName)
371         }
372         
373         state.sensors = sensors
374
375                 return "success"
376         }
377     
378 }
379
380 def createChild(sensorId, sensorName) {
381         log.trace "creating Wattvision Child"
382
383         def child = getChildDevice(sensorId)
384
385         if (child) {
386                 log.warn "Device already exists"
387         } else {
388                 child = addChildDevice("smartthings", "Wattvision", sensorId, null, [name: "Wattvision", label: sensorName])
389         }
390
391         child.setGraphUrl(getGraphUrl(sensorId));
392
393         getDataForChild(child, null, null)
394
395         scheduleDataCollection()
396
397         return childMarshaller(child)
398 }
399
400 // ========================================================
401 // URL HELPERS
402 // ========================================================
403
404 private loginURL() { "${wattvisionBaseURL()}${loginPath()}" }
405
406 private wattvisionBaseURL() { "https://www.wattvision.com" }
407
408 private loginPath() { "/partners/smartthings/login?callback_url=${loginCallbackURL().encodeAsURL()}" }
409
410 private loginCallbackURL() {
411         if (!atomicState.accessToken) { createAccessToken() }
412         buildActionUrl(loginCallbackPath())
413 }
414 private loginCallbackPath() { "login/callback" }
415
416 // ========================================================
417 // Access Token
418 // ========================================================
419
420 private getMyAccessToken() { return atomicState.accessToken ?: createAccessToken() }
421
422 // ========================================================
423 // CONNECTED HTML
424 // ========================================================
425
426 def connectionSuccessful(deviceName, iconSrc) {
427         def html = """
428 <!DOCTYPE html>
429 <html>
430 <head>
431 <meta name="viewport" content="width=640">
432 <title>Withings Connection</title>
433 <style type="text/css">
434         @font-face {
435                 font-family: 'Swiss 721 W01 Thin';
436                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
437                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
438                          url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
439                          url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
440                          url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
441                 font-weight: normal;
442                 font-style: normal;
443         }
444         @font-face {
445                 font-family: 'Swiss 721 W01 Light';
446                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
447                 src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
448                          url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
449                          url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
450                          url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
451                 font-weight: normal;
452                 font-style: normal;
453         }
454         .container {
455                 width: 560px;
456                 padding: 40px;
457                 /*background: #eee;*/
458                 text-align: center;
459         }
460         img {
461                 vertical-align: middle;
462         }
463         img:nth-child(2) {
464                 margin: 0 30px;
465         }
466         p {
467                 font-size: 2.2em;
468                 font-family: 'Swiss 721 W01 Thin';
469                 text-align: center;
470                 color: #666666;
471                 padding: 0 40px;
472                 margin-bottom: 0;
473         }
474 /*
475         p:last-child {
476                 margin-top: 0px;
477         }
478 */
479         span {
480                 font-family: 'Swiss 721 W01 Light';
481         }
482 </style>
483 </head>
484 <body>
485         <div class="container">
486                 <img src="${iconSrc}" alt="${deviceName} icon" />
487                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
488                 <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
489                 <p>Your ${deviceName} is now connected to SmartThings!</p>
490                 <p>Click 'Done' to finish setup.</p>
491         </div>
492 </body>
493 </html>
494 """
495
496         render contentType: 'text/html', data: html
497 }