2 * Copyright 2015 SmartThings
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:
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 // Automatically generated. Make future change here.
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/"]
31 page(name: "rootPage")
35 def sensors = state.sensors
36 def hrefState = sensors ? "complete" : ""
37 def hrefDescription = ""
38 sensors.each { sensorId, sensorName ->
39 hrefDescription += "${sensorName}\n"
42 dynamicPage(name: "rootPage", install: sensors ? true : false, uninstall: true) {
44 href(url: loginURL(), title: "Connect Wattvision Sensors", style: "embedded", description: hrefDescription, state: hrefState)
47 href(url: "https://www.wattvision.com", title: "Learn More About Wattvision", style: "external", description: null)
56 POST : "setApiAccess",
57 DELETE: "revokeApiAccess"
66 path("/device/:sensorId") {
71 POST : "createDevice",
72 DELETE: "deleteDevice"
75 path("/${loginCallbackPath()}") {
84 log.debug "Installed with settings: ${settings}"
90 log.debug "Updated with settings: ${settings}"
98 getDataFromWattvision()
99 scheduleDataCollection()
102 def getDataFromWattvision() {
104 log.trace "Getting data from Wattvision"
106 def children = getChildDevices()
108 log.warn "No children. Not collecting data from Wattviwion"
109 // currently only support one child
113 def endDate = new Date()
116 if (!state.lastUpdated) {
117 // log.debug "no state.lastUpdated"
118 startDate = new Date(hours: endDate.hours - 3)
120 // log.debug "parsing state.lastUpdated"
121 startDate = new Date().parse(smartThingsDateFormat(), state.lastUpdated)
124 state.lastUpdated = endDate.format(smartThingsDateFormat())
126 children.each { child ->
127 getDataForChild(child, startDate, endDate)
132 def getDataForChild(child, startDate, endDate) {
137 def wattvisionURL = wattvisionURL(child.deviceNetworkId, startDate, endDate)
140 httpGet(uri: wattvisionURL) { response ->
141 def json = new org.json.JSONObject(response.data.toString())
142 child.addWattvisionData(json)
145 } catch (groovyx.net.http.HttpResponseException httpE) {
146 log.error "Wattvision getDataForChild HttpResponseException: ${httpE} -> ${httpE.response.data}"
147 //log.debug "wattvisionURL = ${wattvisionURL}"
150 log.error "Wattvision getDataForChild General Exception: ${e}"
151 //log.debug "wattvisionURL = ${wattvisionURL}"
157 def wattvisionURL(senorId, startDate, endDate) {
159 log.trace "getting wattvisionURL"
161 def wattvisionApiAccess = state.wattvisionApiAccess
162 if (!wattvisionApiAccess.id || !wattvisionApiAccess.key) {
170 startDate = new Date(hours: endDate.hours - 3)
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
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())
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}"
198 // log.debug "wattvisionURL: ${url}"
203 state.lastUpdated = new Date().format(smartThingsDateFormat())
206 public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
208 public wattvisionDateFormat() { "yyyy-MM-dd'T'HH:mm:ss" }
210 def childMarshaller(child) {
214 sensor_id: child.deviceNetworkId,
215 location : child.location.name
219 // ========================================================
221 // ========================================================
224 getChildDevices().collect { childMarshaller(it) }
229 log.trace "Getting device"
231 def child = getChildDevice(params.sensorId)
234 httpError(404, "Device not found")
237 return childMarshaller(child)
242 log.trace "Updating Device with data from Wattvision"
244 def body = request.JSON
246 def child = getChildDevice(params.sensorId)
249 httpError(404, "Device not found")
252 child.addWattvisionData(body)
254 render([status: 204, data: " "])
259 log.trace "Creating Wattvision device"
261 if (getChildDevice(params.sensorId)) {
262 httpError(403, "Device already exists")
265 def child = addChildDevice("smartthings", "Wattvision", params.sensorId, null, [name: "Wattvision", label: request.JSON.label])
267 child.setGraphUrl(getGraphUrl(params.sensorId));
269 getDataForChild(child, null, null)
271 return childMarshaller(child)
276 log.trace "Deleting Wattvision device"
278 deleteChildDevice(params.sensorId)
279 render([status: 204, data: " "])
284 log.trace "Granting access to Wattvision API"
286 def body = request.JSON
288 state.wattvisionApiAccess = [
294 scheduleDataCollection()
296 render([status: 204, data: " "])
299 def scheduleDataCollection() {
300 schedule("* /1 * * * ?", "getDataFromWattvision") // every 1 minute
303 def revokeApiAccess() {
305 log.trace "Revoking access to Wattvision API"
307 state.wattvisionApiAccess = [:]
308 render([status: 204, data: " "])
311 public getGraphUrl(sensorId) {
313 log.trace "Collecting URL for Wattvision graph"
315 def apiId = state.wattvisionApiAccess.id
316 def apiKey = state.wattvisionApiAccess.key
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"
322 // ========================================================
323 // SmartThings initiated setup
324 // ========================================================
326 /* Debug info for Steve / Andrew
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.
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?
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"}
346 def loginCallback() {
347 log.trace "loginCallback"
349 state.wattvisionApiAccess = [
354 getSensorJSON(params.id, params.key)
356 connectionSuccessful("Wattvision", "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision@2x.png")
359 private getSensorJSON(id, key) {
360 log.trace "getSensorJSON"
362 def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
364 httpGet(uri: sensorUrl) { response ->
368 response.data.each { sensorId, sensorName ->
369 sensors[sensorId] = sensorName
370 createChild(sensorId, sensorName)
373 state.sensors = sensors
380 def createChild(sensorId, sensorName) {
381 log.trace "creating Wattvision Child"
383 def child = getChildDevice(sensorId)
386 log.warn "Device already exists"
388 child = addChildDevice("smartthings", "Wattvision", sensorId, null, [name: "Wattvision", label: sensorName])
391 child.setGraphUrl(getGraphUrl(sensorId));
393 getDataForChild(child, null, null)
395 scheduleDataCollection()
397 return childMarshaller(child)
400 // ========================================================
402 // ========================================================
404 private loginURL() { "${wattvisionBaseURL()}${loginPath()}" }
406 private wattvisionBaseURL() { "https://www.wattvision.com" }
408 private loginPath() { "/partners/smartthings/login?callback_url=${loginCallbackURL().encodeAsURL()}" }
410 private loginCallbackURL() {
411 if (!atomicState.accessToken) { createAccessToken() }
412 buildActionUrl(loginCallbackPath())
414 private loginCallbackPath() { "login/callback" }
416 // ========================================================
418 // ========================================================
420 private getMyAccessToken() { return atomicState.accessToken ?: createAccessToken() }
422 // ========================================================
424 // ========================================================
426 def connectionSuccessful(deviceName, iconSrc) {
431 <meta name="viewport" content="width=640">
432 <title>Withings Connection</title>
433 <style type="text/css">
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');
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');
457 /*background: #eee;*/
461 vertical-align: middle;
468 font-family: 'Swiss 721 W01 Thin';
480 font-family: 'Swiss 721 W01 Light';
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>
496 render contentType: 'text/html', data: html