Compare commits
1 Commits
user168709
...
MSA-2473-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
749d04d99e |
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* netatmo-basestation Date: 10.07.2017
|
||||
*
|
||||
* Copyright 2014 Brian Steere
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Based on Brian Steere's netatmo-basestation DTH
|
||||
*/
|
||||
|
||||
|
||||
metadata {
|
||||
definition (name: "Netatmo Basestation", namespace: "cscheiene", author: "Brian Steere,cscheiene") {
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Sensor"
|
||||
capability "Carbon Dioxide Measurement"
|
||||
capability "Sound Pressure Level"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "pressure", "number"
|
||||
attribute "min_temp", "number"
|
||||
attribute "max_temp", "number"
|
||||
attribute "temp_trend", "string"
|
||||
attribute "pressure_trend", "string"
|
||||
attribute "lastupdate", "string"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
preferences {
|
||||
input title: "Settings", description: "To change units and time format, go to the Netatmo Connect App", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input title: "Information", description: "Your Netatmo station updates the Netatmo servers approximately every 10 minutes. The Netatmo Connect app polls these servers every 5 minutes. If the time of last update is equal to or less than 10 minutes, pressing the refresh button will have no effect", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
}
|
||||
|
||||
tiles (scale: 2) {
|
||||
multiAttributeTile(name:"main", type:"generic", width:6, height:4) {
|
||||
tileAttribute("temperature", key: "PRIMARY_CONTROL") {
|
||||
attributeState "temperature",label:'${currentValue}°', icon:"st.Weather.weather2", backgroundColors:[
|
||||
[value: 32, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 92, color: "#d04e00"],
|
||||
[value: 98, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
tileAttribute ("humidity", key: "SECONDARY_CONTROL") {
|
||||
attributeState "humidity", label:'Humidity: ${currentValue}%'
|
||||
}
|
||||
}
|
||||
valueTile("temperature", "device.temperature") {
|
||||
state("temperature", label: '${currentValue}°', icon:"st.Weather.weather2", backgroundColors: [
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
valueTile("min_temp", "min_temp", width: 2, height: 1) {
|
||||
state "min_temp", label: 'Min: ${currentValue}°'
|
||||
}
|
||||
valueTile("max_temp", "max_temp", width: 2, height: 1) {
|
||||
state "max_temp", label: 'Max: ${currentValue}°'
|
||||
}
|
||||
valueTile("humidity", "device.humidity", inactiveLabel: false) {
|
||||
state "humidity", label:'${currentValue}%'
|
||||
}
|
||||
valueTile("temp_trend", "temp_trend", width: 4, height: 1) {
|
||||
state "temp_trend", label: 'Temp Trend: ${currentValue}'
|
||||
}
|
||||
valueTile("pressure_trend", "pressure_trend", width: 4, height: 1) {
|
||||
state "pressure_trend", label: 'Press Trend: ${currentValue}'
|
||||
}
|
||||
valueTile("carbonDioxide", "device.carbonDioxide", width: 2, height: 2, inactiveLabel: false) {
|
||||
state "carbonDioxide", label:'${currentValue}ppm', backgroundColors: [
|
||||
[value: 600, color: "#44B621"],
|
||||
[value: 999, color: "#ffcc00"],
|
||||
[value: 1000, color: "#e86d13"]
|
||||
]
|
||||
}
|
||||
valueTile("soundPressureLevel", "device.soundPressureLevel", width: 2, height: 2, inactiveLabel: false) {
|
||||
state "soundPressureLevel", label:'${currentValue}db'
|
||||
}
|
||||
valueTile("pressure", "device.pressure", width: 2, height: 1, inactiveLabel: false) {
|
||||
state "pressure", label:'${currentValue}'
|
||||
}
|
||||
valueTile("units", "units", width: 2, height: 1, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
valueTile("lastupdate", "lastupdate", width: 4, height: 1, inactiveLabel: false) {
|
||||
state "default", label:"Last updated: " + '${currentValue}'
|
||||
}
|
||||
valueTile("date_min_temp", "date_min_temp", width: 2, height: 1, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
valueTile("date_max_temp", "date_max_temp", width: 2, height: 1, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["main"])
|
||||
details(["main","min_temp","date_min_temp","carbonDioxide", "max_temp","date_max_temp", "temp_trend", "soundPressureLevel", "pressure", "units", "pressure_trend", "refresh", "lastupdate"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle 'pressure' attribute
|
||||
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Polling"
|
||||
parent.poll()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing"
|
||||
parent.poll()
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* netatmo-outdoor Date: 10.07.2017
|
||||
*
|
||||
* Copyright 2014 Brian Steere
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Based on Brian Steere's netatmo-outdoor DTH
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Netatmo Outdoor Module", namespace: "cscheiene", author: "Brian Steere,cscheiene") {
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "min_temp", "number"
|
||||
attribute "max_temp", "number"
|
||||
attribute "temp_trend", "string"
|
||||
attribute "lastupdate", "string"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
preferences {
|
||||
input title: "Settings", description: "To change units and time format, go to the Netatmo Connect App", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input title: "Information", description: "Your Netatmo station updates the Netatmo servers approximately every 10 minutes. The Netatmo Connect app polls these servers every 5 minutes. If the time of last update is equal to or less than 10 minutes, pressing the refresh button will have no effect", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
}
|
||||
|
||||
tiles (scale: 2) {
|
||||
multiAttributeTile(name:"main", type:"generic", width:6, height:4) {
|
||||
tileAttribute("temperature", key: "PRIMARY_CONTROL") {
|
||||
attributeState "temperature",label:'${currentValue}°', icon:"st.Weather.weather2", backgroundColors:[
|
||||
[value: 32, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 92, color: "#d04e00"],
|
||||
[value: 98, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
tileAttribute ("humidity", key: "SECONDARY_CONTROL") {
|
||||
attributeState "humidity", label:'Humidity: ${currentValue}%'
|
||||
}
|
||||
}
|
||||
valueTile("min_temp", "min_temp", width: 2, height: 1) {
|
||||
state "min_temp", label: 'Min: ${currentValue}°'
|
||||
}
|
||||
valueTile("max_temp", "max_temp", width: 2, height: 1) {
|
||||
state "max_temp", label: 'Max: ${currentValue}°'
|
||||
}
|
||||
valueTile("temp_trend", "temp_trend", width: 4, height: 1) {
|
||||
state "temp_trend", label: 'Temp Trend: ${currentValue}'
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery_percent", label:'Battery: ${currentValue}%', unit:"", backgroundColors:[
|
||||
[value: 20, color: "#ff0000"],
|
||||
[value: 35, color: "#fd4e3a"],
|
||||
[value: 50, color: "#fda63a"],
|
||||
[value: 60, color: "#fdeb3a"],
|
||||
[value: 75, color: "#d4fd3a"],
|
||||
[value: 90, color: "#7cfd3a"],
|
||||
[value: 99, color: "#55fd3a"]
|
||||
]
|
||||
}
|
||||
valueTile("temperature", "device.temperature") {
|
||||
state("temperature", label: '${currentValue}°', icon:"st.Weather.weather2", backgroundColors: [
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
valueTile("lastupdate", "lastupdate", width: 4, height: 1, inactiveLabel: false) {
|
||||
state "default", label:"Last updated: " + '${currentValue}'
|
||||
}
|
||||
valueTile("date_min_temp", "date_min_temp", width: 2, height: 1, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
valueTile("date_max_temp", "date_max_temp", width: 2, height: 1, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main (["main"])
|
||||
details(["main", "min_temp","date_min_temp", "battery", "max_temp","date_max_temp", "temp_trend", "lastupdate","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
|
||||
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Polling"
|
||||
parent.poll()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing"
|
||||
parent.poll()
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
/**
|
||||
* Netatmo Connect Date: 05.08.2017
|
||||
*/
|
||||
|
||||
import java.text.DecimalFormat
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
private getApiUrl() { "https://api.netatmo.com" }
|
||||
private getVendorName() { "netatmo" }
|
||||
private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" }
|
||||
private getVendorTokenPath(){ "${apiUrl}/oauth2/token" }
|
||||
private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" }
|
||||
private getClientId() { appSettings.clientId }
|
||||
private getClientSecret() { appSettings.clientSecret }
|
||||
private getServerUrl() { appSettings.serverUrl }
|
||||
private getShardUrl() { return getApiServerUrl() }
|
||||
private getCallbackUrl() { "${serverUrl}/oauth/callback" }
|
||||
private getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
|
||||
|
||||
// Automatically generated. Make future change here.
|
||||
definition(
|
||||
name: "Netatmo (Connect) Modified",
|
||||
namespace: "cscheiene",
|
||||
author: "Brian Steere,cscheiene",
|
||||
description: "Netatmo Integration",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
|
||||
oauth: true,
|
||||
singleInstance: true
|
||||
){
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
preferences {
|
||||
page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false)
|
||||
page(name: "listDevices", title: "Netatmo Devices", content: "listDevices", install: false)
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||
path("/oauth/callback") {action: [GET: "callback"]}
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
log.debug "In authPage"
|
||||
|
||||
def description
|
||||
def uninstallAllowed = false
|
||||
def oauthTokenProvided = false
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token."
|
||||
state.accessToken = createAccessToken()
|
||||
}
|
||||
|
||||
if (canInstallLabs()) {
|
||||
|
||||
def redirectUrl = getBuildRedirectUrl()
|
||||
// log.debug "Redirect url = ${redirectUrl}"
|
||||
|
||||
if (state.authToken) {
|
||||
description = "Tap 'Next' to proceed"
|
||||
uninstallAllowed = true
|
||||
oauthTokenProvided = true
|
||||
} else {
|
||||
description = "Click to enter Credentials."
|
||||
}
|
||||
|
||||
if (!oauthTokenProvided) {
|
||||
log.debug "Show the login page"
|
||||
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
|
||||
section() {
|
||||
paragraph "Tap below to log in to the netatmo and authorize SmartThings access."
|
||||
href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "Show the devices page"
|
||||
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
|
||||
section() {
|
||||
input(name:"Devices", style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def oauthInitUrl() {
|
||||
log.debug "In oauthInitUrl"
|
||||
|
||||
state.oauthInitState = UUID.randomUUID().toString()
|
||||
|
||||
def oauthParams = [
|
||||
response_type: "code",
|
||||
client_id: getClientId(),
|
||||
client_secret: getClientSecret(),
|
||||
state: state.oauthInitState,
|
||||
redirect_uri: getCallbackUrl(),
|
||||
scope: "read_station"
|
||||
]
|
||||
|
||||
// log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
|
||||
|
||||
redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
|
||||
}
|
||||
|
||||
def callback() {
|
||||
// log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||
|
||||
def code = params.code
|
||||
def oauthState = params.state
|
||||
|
||||
if (oauthState == state.oauthInitState) {
|
||||
|
||||
def tokenParams = [
|
||||
client_secret: getClientSecret(),
|
||||
client_id : getClientId(),
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: getCallbackUrl(),
|
||||
code: code,
|
||||
scope: "read_station"
|
||||
]
|
||||
|
||||
// log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
|
||||
|
||||
def tokenUrl = getVendorTokenPath()
|
||||
def params = [
|
||||
uri: tokenUrl,
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
body: tokenParams
|
||||
]
|
||||
|
||||
// log.debug "PARAMS: ${params}"
|
||||
|
||||
httpPost(params) { resp ->
|
||||
|
||||
def slurper = new JsonSlurper()
|
||||
|
||||
resp.data.each { key, value ->
|
||||
def data = slurper.parseText(key)
|
||||
|
||||
state.refreshToken = data.refresh_token
|
||||
state.authToken = data.access_token
|
||||
state.tokenExpires = now() + (data.expires_in * 1000)
|
||||
// log.debug "swapped token: $resp.data"
|
||||
}
|
||||
}
|
||||
|
||||
// Handle success and failure here, and render stuff accordingly
|
||||
if (state.authToken) {
|
||||
success()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
|
||||
} else {
|
||||
log.error "callback() failed oauthState != state.oauthInitState"
|
||||
}
|
||||
}
|
||||
|
||||
def success() {
|
||||
log.debug "in success"
|
||||
def message = """
|
||||
<p>We have located your """ + getVendorName() + """ account.</p>
|
||||
<p>Tap 'Done' to continue to Devices.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def fail() {
|
||||
log.debug "in fail"
|
||||
def message = """
|
||||
<p>The connection could not be established!</p>
|
||||
<p>Click 'Done' to return to the menu.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def connectionStatus(message, redirectUrl = null) {
|
||||
def redirectHtml = ""
|
||||
if (redirectUrl) {
|
||||
redirectHtml = """
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${getVendorName()} Connection</title>
|
||||
<style type="text/css">
|
||||
* { box-sizing: border-box; }
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
${message}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
def refreshToken() {
|
||||
log.debug "In refreshToken"
|
||||
|
||||
def oauthParams = [
|
||||
client_secret: getClientSecret(),
|
||||
client_id: getClientId(),
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: state.refreshToken
|
||||
]
|
||||
|
||||
def tokenUrl = getVendorTokenPath()
|
||||
def params = [
|
||||
uri: tokenUrl,
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
body: oauthParams,
|
||||
]
|
||||
|
||||
// OAuth Step 2: Request access token with our client Secret and OAuth "Code"
|
||||
try {
|
||||
httpPost(params) { response ->
|
||||
def slurper = new JsonSlurper();
|
||||
|
||||
response.data.each {key, value ->
|
||||
def data = slurper.parseText(key);
|
||||
// log.debug "Data: $data"
|
||||
|
||||
state.refreshToken = data.refresh_token
|
||||
state.accessToken = data.access_token
|
||||
state.tokenExpires = now() + (data.expires_in * 1000)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug "Error: $e"
|
||||
}
|
||||
|
||||
// We didn't get an access token
|
||||
if ( !state.accessToken ) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initialized with settings: ${settings}"
|
||||
|
||||
// Pull the latest device info into state
|
||||
getDeviceList();
|
||||
|
||||
settings.devices.each {
|
||||
def deviceId = it
|
||||
def detail = state?.deviceDetail[deviceId]
|
||||
|
||||
try {
|
||||
switch(detail?.type) {
|
||||
case 'NAMain':
|
||||
log.debug "Creating Base station"
|
||||
createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name)
|
||||
break
|
||||
case 'NAModule1':
|
||||
log.debug "Creating Outdoor module"
|
||||
createChildDevice("Netatmo Outdoor Module", deviceId, "${detail.type}.${deviceId}", detail.module_name)
|
||||
break
|
||||
case 'NAModule3':
|
||||
log.debug "Creating Rain Gauge"
|
||||
createChildDevice("Netatmo Rain", deviceId, "${detail.type}.${deviceId}", detail.module_name)
|
||||
break
|
||||
case 'NAModule4':
|
||||
log.debug "Creating Additional module"
|
||||
createChildDevice("Netatmo Additional Module", deviceId, "${detail.type}.${deviceId}", detail.module_name)
|
||||
break
|
||||
case 'NAModule2':
|
||||
log.debug "Creating Wind module"
|
||||
createChildDevice("Netatmo Wind", deviceId, "${detail.type}.${deviceId}", detail.module_name)
|
||||
break
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error "Error creating device: ${e}"
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup any other devices that need to go away
|
||||
def delete = getChildDevices().findAll { !settings.devices.contains(it.deviceNetworkId) }
|
||||
log.debug "Delete: $delete"
|
||||
delete.each { deleteChildDevice(it.deviceNetworkId) }
|
||||
|
||||
// Do the initial poll
|
||||
poll()
|
||||
// Schedule it to run every 5 minutes
|
||||
runEvery5Minutes("poll")
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
log.debug "In uninstalled"
|
||||
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
def getDeviceList() {
|
||||
log.debug "Refreshing station data"
|
||||
def deviceList = [:]
|
||||
def moduleName = null
|
||||
state.deviceDetail = [:]
|
||||
state.deviceState = [:]
|
||||
|
||||
apiGet("/api/getstationsdata",["get_favorites":true]) { resp ->
|
||||
state.response = resp.data.body
|
||||
resp.data.body.devices.each { value ->
|
||||
def key = value._id
|
||||
if (value.module_name != null) {
|
||||
deviceList[key] = "${value.station_name}: ${value.module_name}"
|
||||
state.deviceDetail[key] = value
|
||||
state.deviceState[key] = value.dashboard_data
|
||||
}
|
||||
|
||||
value.modules.each { value2 ->
|
||||
def key2 = value2._id
|
||||
|
||||
if (value2.module_name != null) {
|
||||
deviceList[key2] = "${value.station_name}: ${value2.module_name}"
|
||||
state.deviceDetail[key2] = value2
|
||||
state.deviceState[key2] = value2.dashboard_data
|
||||
}
|
||||
else {
|
||||
switch(value2.type) {
|
||||
case "NAModule1":
|
||||
moduleName = "Outdoor ${value.station_name}"
|
||||
break
|
||||
case "NAModule2":
|
||||
moduleName = "Wind ${value.station_name}"
|
||||
break
|
||||
case "NAModule3":
|
||||
moduleName = "Rain ${value.station_name}"
|
||||
break
|
||||
case "NAModule4":
|
||||
moduleName = "Additional ${value.station_name}"
|
||||
break
|
||||
}
|
||||
|
||||
deviceList[key2] = "${value.station_name}: ${moduleName}"
|
||||
state.deviceDetail[key2] = value2 << ["module_name" : moduleName]
|
||||
state.deviceState[key2] = value2.dashboard_data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deviceList.sort() { it.value.toLowerCase() }
|
||||
|
||||
}
|
||||
|
||||
|
||||
private removeChildDevices(delete) {
|
||||
log.debug "In removeChildDevices"
|
||||
|
||||
log.debug "deleting ${delete.size()} devices"
|
||||
|
||||
delete.each {
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
|
||||
def createChildDevice(deviceFile, dni, name, label) {
|
||||
log.debug "In createChildDevice"
|
||||
|
||||
try {
|
||||
def existingDevice = getChildDevice(dni)
|
||||
if(!existingDevice) {
|
||||
log.debug "Creating child"
|
||||
def childDevice = addChildDevice("cscheiene", deviceFile, dni, null, [name: name, label: label, completedSetup: true])
|
||||
} else {
|
||||
log.debug "Device $dni already exists"
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "Error creating device: ${e}"
|
||||
}
|
||||
}
|
||||
|
||||
def listDevices() {
|
||||
log.debug "In listDevices"
|
||||
|
||||
def devices = getDeviceList()
|
||||
|
||||
dynamicPage(name: "listDevices", title: "Choose devices", install: true) {
|
||||
section("Devices") {
|
||||
input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices
|
||||
}
|
||||
|
||||
section("Preferences") {
|
||||
input "rainUnits", "enum", title: "Rain Units", description: "Please select rain units", required: true, options: [mm:'Millimeters', in:'Inches']
|
||||
input "pressUnits", "enum", title: "Pressure Units", description: "Please select pressure units", required: true, options: [mbar:'mbar', inhg:'inhg']
|
||||
input "windUnits", "enum", title: "Wind Units", description: "Please select wind units", required: true, options: [kph:'kph', ms:'ms', mph:'mph', kts:'kts']
|
||||
input "time", "enum", title: "Time Format", description: "Please select time format", required: true, options: [12:'12 Hour', 24:'24 Hour']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def apiGet(String path, Map query, Closure callback) {
|
||||
if(now() >= state.tokenExpires) {
|
||||
refreshToken();
|
||||
}
|
||||
|
||||
query['access_token'] = state.accessToken
|
||||
def params = [
|
||||
uri: getApiUrl(),
|
||||
path: path,
|
||||
'query': query
|
||||
]
|
||||
// log.debug "API Get: $params"
|
||||
|
||||
try {
|
||||
httpGet(params) { response ->
|
||||
callback.call(response)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// This is most likely due to an invalid token. Try to refresh it and try again.
|
||||
log.debug "apiGet: Call failed $e"
|
||||
if(refreshToken()) {
|
||||
log.debug "apiGet: Trying again after refreshing token"
|
||||
httpGet(params) { response ->
|
||||
callback.call(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def apiGet(String path, Closure callback) {
|
||||
apiGet(path, [:], callback);
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Polling"
|
||||
getDeviceList();
|
||||
def children = getChildDevices()
|
||||
//log.debug "State: ${state.deviceState}"
|
||||
|
||||
settings.devices.each { deviceId ->
|
||||
def detail = state?.deviceDetail[deviceId]
|
||||
def data = state?.deviceState[deviceId]
|
||||
def child = children?.find { it.deviceNetworkId == deviceId }
|
||||
|
||||
//log.debug "Update: $child";
|
||||
switch(detail?.type) {
|
||||
case 'NAMain':
|
||||
log.debug "Updating NAMain $data"
|
||||
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'carbonDioxide', value: data['CO2'], unit: "ppm")
|
||||
child?.sendEvent(name: 'humidity', value: data['Humidity'], unit: "%")
|
||||
child?.sendEvent(name: 'temp_trend', value: data['temp_trend'], unit: "")
|
||||
child?.sendEvent(name: 'pressure', value: (pressToPref(data['Pressure'])).toDouble().trunc(2), unit: settings.pressUnits)
|
||||
child?.sendEvent(name: 'soundPressureLevel', value: data['Noise'], unit: "db")
|
||||
child?.sendEvent(name: 'pressure_trend', value: data['pressure_trend'], unit: "")
|
||||
child?.sendEvent(name: 'min_temp', value: cToPref(data['min_temp']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'max_temp', value: cToPref(data['max_temp']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'units', value: settings.pressUnits)
|
||||
child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']), unit: "")
|
||||
child?.sendEvent(name: 'date_min_temp', value: lastUpdated(data['date_min_temp']), unit: "")
|
||||
child?.sendEvent(name: 'date_max_temp', value: lastUpdated(data['date_max_temp']), unit: "")
|
||||
break;
|
||||
case 'NAModule1':
|
||||
log.debug "Updating NAModule1 $data"
|
||||
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'humidity', value: data['Humidity'], unit: "%")
|
||||
child?.sendEvent(name: 'temp_trend', value: data['temp_trend'], unit: "")
|
||||
child?.sendEvent(name: 'min_temp', value: cToPref(data['min_temp']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'max_temp', value: cToPref(data['max_temp']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'battery', value: detail['battery_percent'], unit: "%")
|
||||
child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']), unit: "")
|
||||
child?.sendEvent(name: 'date_min_temp', value: lastUpdated(data['date_min_temp']), unit: "")
|
||||
child?.sendEvent(name: 'date_max_temp', value: lastUpdated(data['date_max_temp']), unit: "")
|
||||
break;
|
||||
case 'NAModule3':
|
||||
log.debug "Updating NAModule3 $data"
|
||||
child?.sendEvent(name: 'rain', value: (rainToPref(data['Rain'])), unit: settings.rainUnits)
|
||||
child?.sendEvent(name: 'rainSumHour', value: (rainToPref(data['sum_rain_1'])), unit: settings.rainUnits)
|
||||
child?.sendEvent(name: 'rainSumDay', value: (rainToPref(data['sum_rain_24'])), unit: settings.rainUnits)
|
||||
child?.sendEvent(name: 'units', value: settings.rainUnits)
|
||||
child?.sendEvent(name: 'battery', value: detail['battery_percent'], unit: "%")
|
||||
child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']), unit: "")
|
||||
child?.sendEvent(name: 'rainUnits', value: rainToPrefUnits(data['Rain']), displayed: false)
|
||||
child?.sendEvent(name: 'rainSumHourUnits', value: rainToPrefUnits(data['sum_rain_1']), displayed: false)
|
||||
child?.sendEvent(name: 'rainSumDayUnits', value: rainToPrefUnits(data['sum_rain_24']), displayed: false)
|
||||
break;
|
||||
case 'NAModule4':
|
||||
log.debug "Updating NAModule4 $data"
|
||||
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'carbonDioxide', value: data['CO2'], unit: "ppm")
|
||||
child?.sendEvent(name: 'humidity', value: data['Humidity'], unit: "%")
|
||||
child?.sendEvent(name: 'temp_trend', value: data['temp_trend'], unit: "")
|
||||
child?.sendEvent(name: 'min_temp', value: cToPref(data['min_temp']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'max_temp', value: cToPref(data['max_temp']) as float, unit: getTemperatureScale())
|
||||
child?.sendEvent(name: 'battery', value: detail['battery_percent'], unit: "%")
|
||||
child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']), unit: "")
|
||||
child?.sendEvent(name: 'date_min_temp', value: lastUpdated(data['date_min_temp']), unit: "")
|
||||
child?.sendEvent(name: 'date_max_temp', value: lastUpdated(data['date_max_temp']), unit: "")
|
||||
break;
|
||||
case 'NAModule2':
|
||||
log.debug "Updating NAModule2 $data"
|
||||
child?.sendEvent(name: 'WindAngle', value: data['WindAngle'], unit: "°", displayed: false)
|
||||
child?.sendEvent(name: 'GustAngle', value: data['GustAngle'], unit: "°", displayed: false)
|
||||
child?.sendEvent(name: 'battery', value: detail['battery_percent'], unit: "%")
|
||||
child?.sendEvent(name: 'WindStrength', value: (windToPref(data['WindStrength'])).toDouble().trunc(1), unit: settings.windUnits)
|
||||
child?.sendEvent(name: 'GustStrength', value: (windToPref(data['GustStrength'])).toDouble().trunc(1), unit: settings.windUnits)
|
||||
child?.sendEvent(name: 'max_wind_str', value: (windToPref(data['max_wind_str'])).toDouble().trunc(1), unit: settings.windUnits)
|
||||
child?.sendEvent(name: 'units', value: settings.windUnits)
|
||||
child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']), unit: "")
|
||||
child?.sendEvent(name: 'date_max_wind_str', value: lastUpdated(data['date_max_wind_str']), unit: "")
|
||||
child?.sendEvent(name: 'WindDirection', value: windTotext(data['WindAngle']))
|
||||
child?.sendEvent(name: 'GustDirection', value: gustTotext(data['GustAngle']))
|
||||
child?.sendEvent(name: 'WindStrengthUnits', value: windToPrefUnits(data['WindStrength']), displayed: false)
|
||||
child?.sendEvent(name: 'GustStrengthUnits', value: windToPrefUnits(data['GustStrength']), displayed: false)
|
||||
child?.sendEvent(name: 'max_wind_strUnits', value: windToPrefUnits(data['max_wind_str']), displayed: false)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def cToPref(temp) {
|
||||
if(getTemperatureScale() == 'C') {
|
||||
return temp
|
||||
} else {
|
||||
return temp * 1.8 + 32
|
||||
}
|
||||
}
|
||||
|
||||
def rainToPref(rain) {
|
||||
if(settings.rainUnits == 'mm') {
|
||||
return rain.toDouble().trunc(1)
|
||||
} else {
|
||||
return (rain * 0.039370).toDouble().trunc(3)
|
||||
}
|
||||
}
|
||||
|
||||
def rainToPrefUnits(rain) {
|
||||
if(settings.rainUnits == 'mm') {
|
||||
return rain.toDouble().trunc(1) + " mm"
|
||||
} else {
|
||||
return (rain * 0.039370).toDouble().trunc(3) + " in"
|
||||
}
|
||||
}
|
||||
|
||||
def pressToPref(Pressure) {
|
||||
if(settings.pressUnits == 'mbar') {
|
||||
return Pressure
|
||||
} else {
|
||||
return Pressure * 0.029530
|
||||
}
|
||||
}
|
||||
|
||||
def windToPref(Wind) {
|
||||
if(settings.windUnits == 'kph') {
|
||||
return Wind
|
||||
} else if (settings.windUnits == 'ms') {
|
||||
return Wind * 0.277778
|
||||
} else if (settings.windUnits == 'mph') {
|
||||
return Wind * 0.621371192
|
||||
} else if (settings.windUnits == 'kts') {
|
||||
return Wind * 0.539956803
|
||||
}
|
||||
}
|
||||
|
||||
def windToPrefUnits(Wind) {
|
||||
if(settings.windUnits == 'kph') {
|
||||
return Wind
|
||||
} else if (settings.windUnits == 'ms') {
|
||||
return (Wind * 0.277778).toDouble().trunc(1) +" ms"
|
||||
} else if (settings.windUnits == 'mph') {
|
||||
return (Wind * 0.621371192).toDouble().trunc(1) +" mph"
|
||||
} else if (settings.windUnits == 'kts') {
|
||||
return (Wind * 0.539956803).toDouble().trunc(1) +" kts"
|
||||
}
|
||||
}
|
||||
def lastUpdated(time) {
|
||||
if(settings.time == '24') {
|
||||
def updtTime = new Date(time*1000L).format("HH:mm", location.timeZone)
|
||||
state.lastUpdated = updtTime
|
||||
return updtTime
|
||||
} else {
|
||||
def updtTime = new Date(time*1000L).format("h:mm aa", location.timeZone)
|
||||
state.lastUpdated = updtTime
|
||||
return updtTime
|
||||
}
|
||||
}
|
||||
|
||||
def windTotext(WindAngle) {
|
||||
if(WindAngle < 23) {
|
||||
return WindAngle + "° North"
|
||||
} else if (WindAngle < 68) {
|
||||
return WindAngle + "° NorthEast"
|
||||
} else if (WindAngle < 113) {
|
||||
return WindAngle + "° East"
|
||||
} else if (WindAngle < 158) {
|
||||
return WindAngle + "° SouthEast"
|
||||
} else if (WindAngle < 203) {
|
||||
return WindAngle + "° South"
|
||||
} else if (WindAngle < 248) {
|
||||
return WindAngle + "° SouthWest"
|
||||
} else if (WindAngle < 293) {
|
||||
return WindAngle + "° West"
|
||||
} else if (WindAngle < 338) {
|
||||
return WindAngle + "° NorthWest"
|
||||
} else if (WindAngle < 361) {
|
||||
return WindAngle + "° North"
|
||||
}
|
||||
}
|
||||
|
||||
def gustTotext(GustAngle) {
|
||||
if(GustAngle < 23) {
|
||||
return GustAngle + "° North"
|
||||
} else if (GustAngle < 68) {
|
||||
return GustAngle + "° NEast"
|
||||
} else if (GustAngle < 113) {
|
||||
return GustAngle + "° East"
|
||||
} else if (GustAngle < 158) {
|
||||
return GustAngle + "° SEast"
|
||||
} else if (GustAngle < 203) {
|
||||
return GustAngle + "° South"
|
||||
} else if (GustAngle < 248) {
|
||||
return GustAngle + "° SWest"
|
||||
} else if (GustAngle < 293) {
|
||||
return GustAngle + "° West"
|
||||
} else if (GustAngle < 338) {
|
||||
return GustAngle + "° NWest"
|
||||
} else if (GustAngle < 361) {
|
||||
return GustAngle + "° North"
|
||||
}
|
||||
}
|
||||
|
||||
def debugEvent(message, displayEvent) {
|
||||
|
||||
def results = [
|
||||
name: "appdebug",
|
||||
descriptionText: message,
|
||||
displayed: displayEvent
|
||||
]
|
||||
log.debug "Generating AppDebug Event: ${results}"
|
||||
sendEvent (results)
|
||||
|
||||
}
|
||||
|
||||
private Boolean canInstallLabs() {
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
private List getRealHubFirmwareVersions() {
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
Reference in New Issue
Block a user