Compare commits

...

1 Commits

Author SHA1 Message Date
Stephane Mora
749d04d99e MSA-2473: please do something 2017-12-22 13:47:36 -08:00
3 changed files with 1000 additions and 0 deletions

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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 }
}