Netatmo Connect (ported from ST)

So I went ahead and tried to port over the Netatmo ST app and drivers into hubitat.

I am greeted with the following log upon install:

**[app:770](http://192.168.7.201/logs#app770)2018-04-14 08:43:28.739:errorCannot get property 'serverUrl' on null object on line 15**

[app:770](http://192.168.7.201/logs#app770)2018-04-14 08:43:28.721:debugAbout to create access token.

[app:770](http://192.168.7.201/logs#app770)2018-04-14 08:43:28.709:debugIn authPage

Any ideas?

Here is the app:

 /**
 * Netatmo Connect Date: 12.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']
            input "sound", "number", title: "Sound Sensor: \nEnter the value when sound will be marked as detected", description: "Please enter number", required: false
        }
	}
}

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: 'sound', value: noiseTosound(data['Noise']))
                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 noiseTosound(Noise) {
	if(Noise > settings.sound) { 
    	return "detected"
    } else {
    	return "not detected"
    }
}

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 }
}
1 Like

So in examining what needs to change it has to do with the ability to enter in the equivalent of the ST shard.

In ST I can enter these settings prior to installing the app which then eventually links me to the netatmo login page to authorize the ST app.

I wouldn't know where to even begin with entering in this information in hubitat.

I’m watching closely because I was playing with porting the Harmony app and it had the same variables that I eventually gave up on. Let me know if you figure it out. I gave up after an evening of trying.

So I’ve managed to get to the login screen by manually inserting the hub IP address and access tokens directly into the app code. However, when I try to login to Netatmo I get the 404 error.

ANy ideas? Please note the url as I would think I would need to be re-directed to the netatmo site.

I also notice when porting over the Neato Smartvac app/driver, I get almost the exact identical message.

So I have to be doing something wrong in that I am not point at the correct IP address during the oauth process?

Can anyone jump in and save me?

I’m going to keep talking myself into circles until I stumble upon a possible solution that someone can assist me with.

I am certain the Netatmo API url is correct, so what I am thinking is the ServerUrl() and the ShardUrl() [same thing] needs to point to the HE cloud endpoint rather to my LAN?

Would that be a pathway to success?

This is one on my list also… Will take a look this weekend. But yes you need the cloud API not the local as the netatmo is a cloud based product. It cannot access the local address. My tado connect smartapp uses the cloud API if it helps to review my code

1 Like

Any movement here? This is the last device I have to connect to HE.

It’s in here.

I just purchased some Netatmo cameras and I would like to add them to hubitat. Did anyone figure this out?

Any success on this folks? I am tracking this because I am trying to port my ST hub to hubitat. Anxious to hear news on your success.

LJ

Netatmo is a bag of hurt on HE. It kinda was on ST as well. Whatever port was done does not play nicely with HE and the http commands that get thrown across.

My advice: go out and install WEEWX. It's open source, free to download/install and update and you only need a RPI to do so.

Total install time and to install the Netatmo drivers was about 3 hours (with learning curve). Then you can go and install the 'Weather Switch App' and you will have integration into HE with netatmo without clogging up HE.

Interesting approach. I just got my HE about a week ago and I am still planning how I am going to migrate my 167 devices, webcore, and numerous smart apps. I haven't gone down the RPI route yet but my thinking is slowly headed that way just to run Webcore dashboard locally. However, my thinking process to date is if i set up a RPI I might be better off setting up a HomeSeer hub or some other hub software on it and get the same benefits of local operation as HE. I just fear that HomeSeer doesn't have the active community support that St and HE does. Any thoughts on that.

LJ

I have the fuzzysb version of "Netatmo (Connect)" installed... And it works perfectly...

The device drivers are HERE and the app is HERE.

EDIT: An updated application/driver package is now available.

Thanks, That doesn't seem to come up on my search. I will give it a try.

All the current states of each device seem to read correctly. One question so far. Why does the base station, outdoor module and additional module appear as a thermostat with all the associated heat,Air, and fan controls. That doesn't seem right and certainly is not how the Netatmo Connect worked on Smartthings. I haven't tried dashboard yet to see how it appears there.

LJ

Some have better luck with Netatmo than others and I agree the Netatmo app works. From my experience though I began to see hub slow downs and crashes on HE because if the Netatmo cloud would go out (or if my Netatmo hub went off wi-fi), HE would go nuts.

So... if you experience slow downs this is why.

HI when trying to save the connect driver i get
No signature of method: Script1.definition() is applicable for argument types: (java.util.LinkedHashMap) values: [[name:Netatmo (Connect), namespace:fuzzysb, author:Stuart Buchanan, ...]]

the others save just fine. Prob doing something really stupid. Any ideas?

1 Like

Doh... now installed as an app!