Netatmo Weather Station – prywatna stacja pogody dla SmartThings

Jak już wspomnieliśmy pogoda wpływa na nasze codzienne życie. Wprawdzie nie mamy wpływu ani możliwości aby ją zmienić, ale możemy się do niej przystosowywać. A dzięki prognozie pogody nawet przygotować, w razie radykalniejszych zjawisk pogodowych. Dzisiaj przyjrzymy się prywatnej stacji meteorologicznej Netatmo Weather Station.

Recenzja powstała dzięki uprzejmości firmy Netatmo, która wypożyczyła sprzęt na testy.

Moduł wewnętrzny i zewnętrzny stacji pogody.

Jeśli chcemy aby nasz dom i jego otoczenie były bezpieczne i przyjazne dla każdego z domowników potrzebujemy inteligentnej stacji pogody, która zapewni potrzebne informacje dotyczące środowiska, w którym żyjemy w każdym momencie dnia i nocy. Stale możemy sprawdzać bieżącą sytuację, ale także możesz analizować dane zebrane na przestrzeni czasu. Wiedza ta pozwoli efektywniej dbać zdrowie najbliższych, a także lepiej przystosować się do warunków otoczenia. Ze stacja pogody Netatmo dowiemy się, kiedy najlepiej przewietrzyć dom, a kiedy trzeba zabezpieczyć lekkie przedmioty pozostawione na zewnątrz. Dzięki inteligentnej stacji pogodowej możemy sprawdzić parametry pogodowe dla najbliższego otoczenia domu.

Opis

Inteligentna stacja pogody Netatmo Weather Station pozwala na pomiary temperatury w pomieszczeniach w zakresie 0°C do 50°C i dokładnością ± 0,3°C, temperatury na zewnątrz w zakresie -40°C do 65°C i dokładnością ± 0,3°C oraz pomiary wilgotności (w pomieszczeniach i na zewnątrz) w zakresie pomiaru: 0% do 100% dokładnością ± 3%. Barometr pozwala ma pomiar ciśnienia atmosferycznego w zakresie od 260 do 1260 mbarów i posiada dokładność ± 1 mbar. Czujnik CO2 w pomieszczeniu dokonuje pomiaru od 0 do 5000 ppm i utrzymuje dokładność na poziomie ± 50 ppm (przy pomiarach 0 do 1000 ppm) lub ± 5% (dla wartości 1000 do 5000 ppm). Miernik poziomu dźwięku działa w zakresie 35 do 120 dB.

Stację pogody Netatmo Weather Station tworzą dwa podstawowe moduły: główna stacja oraz moduł zewnętrzy do montażu na tarasie. Dodatkowo mamy możliwość dokupić wiatromierz, deszczomierz oraz do dodatkowego pokoju mniejszy moduł wewnętrzny. Moduł główny i zewnętrzny wykonane są z jednoczęściowego srebrnego bloku aluminium, dodatkowo odpornego na promieniowanie UV, dzięki temu stacja wygląda bardzo elegancko. Na szczycie głównego modułu umiejscowiono przycisk, który po kliknięciu dokonuje aktualizacji pomiarów, a dodatkowo poinformuje poprzez diodą LED o jakości powietrza w pomieszczeniu. Posiada małe rozmiary – dla modułu głównego to 15,5 cm wysokości, zaś dla zewnętrznego to tylko 10,5 cm. Szerokość to 4,5 cm i głębokość 4,5 cm jest identyczna dla obu modułów. Stacja główna zasilana jest z gniazdka, zaś moduł na dwór poprzez dwie baterie AAA (do 2 lat działania). Komunikacja odbywa się za pomocą protokołu Wi-Fi 802.11 b/g/n (2,4 GHz) z zasięgiem do 100 oraz zabezpieczeniami WEP/WPA /WPA2-personal. Do poprawnego działania konieczny jest ciągły dostęp do szerokopasmowego Internetu.

W aplikacji Netatmo mamy również 7 dniową prognozę pogody na najbliższe dni, mapę okolicznych stacji pogody, możliwość graficznej analizy wykresów z danych archiwalnych pomiarów oraz graficzne odwzorowanie aktualnej pogody.

Jeśli chcemy rozbudowywać naszą stację meteorologiczną od Netatmo, pamiętajmy, że maksymalnie możemy rozbudować ją o trzy dodatkowe moduły do pokojów, jeden deszczomierz i wiatromierz.

Pełny zestaw Netatmo Weather Station.
Od lewej dodatkowy czujnik wewnętrzny, stacja bazowa, moduł zewnętrzny, wiatromierz oraz deszczomierz.
  • Plusy
    • mnogość pomiarów
    • pomiar na żądanie
    • czujnik CO2
    • stylowy wygląd
    • solidny, odporny oraz kompaktowe wymiary
  • Minusy
    • przetwarzanie w chmurze
    • brak natywnej integracji

Współpraca z SmartThings

W pierwszej kolejności przyjrzymy się stacji bazowej Netatmo. W głównym panelu modułu mamy informację o odczytanej temperaturze pokojowej, wilgotności, stężeniu dwutlenku węgla, poziomu hałasu i stan czujnika dźwięku. Jest tam również informacja o trendzie temperatury – czy spada „down”, rośnie „up” ,czy stabilna „stable” oraz „Todays Min Temp”, czyli dzisiejsza minimalna temperatura w pokoju. Kolejna belka podaje godzinę rejestracji minimalnej temperatury. Dalej mamy „Todays Max Temp” – czyli dzisiejsza maksymalna temperatura w pokoju. Poniżej godzina rejestracji maksymalnej temperatury. Kolejne parametry to ciśnienie atmosferyczne „Atmospheric pressure” oraz jego trend. Ostatnia informacja to godzina ostatniej aktualizacji pomiarów. Wskaźniki „Todays Min Temp” i „Todays Max Temp” są resetowane każdego dnia o północy.

Panel „Historii” również nie różni się znacznie od innych sensorów, mamy dostęp do sortowania i przestudiowania historii działań urządzenia. W ustawieniach „Edytuj” mamy standardową możliwość zmiany nazwy, lokalizacji oraz pokoju. W ustawieniach mamy podaną informację, gdzie zmienić jednostki oraz okres odświeżania danych.

Zewnętrzny moduł stacji pogody Netatmo, w głównym panelu informuje o zmierzonej temperaturze i wilgotności powietrza. Kolejny parametr to trend temperatury – czy spada „down”, rośnie „up”, czy jest stabilna „stable”. „Todays Min Temp”, czyli dzisiejsza minimalna temperatura na dworze. Kolejna belka podaje godzinę rejestracji minimalnej temperatury. Dalej mamy „Todays Max Temp” – czyli dzisiejsza maksymalna temperatura na zewnątrz, a poniżej godzina rejestracji maksymalnej temperatury. Wskaźniki resetują się każdego dnia o północy. Przed ostatnia belka informuje o czasie ostatniej aktualizacji pomiarów. Na koniec mamy informację o procentowym zużyciu baterii.

Stacja Netatmo Weather w ekosystemie SmartThings wykonuje przetwarzanie poleceń w chmurze, z tego też tytułu przy utracie połączenia z Internetem, nie jest możliwe wykonywanie aktualizacji parametrów, jak i zaprogramowanych powiadomień.

Główny moduł stacji meteorologicznej przy tworzeniu automatyzacji „Jeśli … to Wtedy” pozwala na wybranie kolejno (zakresy w nawiasie): temperatury (-20 – 50°C), wilgotności (0-100%), min temp (-20 – 50°C), max temp (-20 – 50°C), czujnik dźwięku (Brak dźwięku/Wykryto dźwięk), stężenie CO2 (0 – 1 000 000ppm), poziom hałasu (0 -194dB), ciśnienie atmosferyczne (0 – 110mbar) – tutaj ewidentny błąd kodu programu, temperature trend (stable, down, up), pressure trend (stable, down, up). Ze względu na specyfikę urządzenia, możliwe jest tylko wykorzystanie warunku „jeśli”. 

Moduł zewnętrzny stacji pogody może również być użyty w automatyzacjach. Przy tworzeniu automatyzacji „Jeśli … to Wtedy” pozwala na wybranie kolejno (zakresy w nawiasie): temperatury (-20 – 50°C), wilgotności (0-100%), trendu temperatury (stable, down, up), odczytu zużycia baterii (0 – 100%), min temp (-20 – 50°C) i max temp (-20 – 50°C). Ze względu na specyfikę urządzenia, nie jest możliwe wykorzystanie warunku „wtedy”. 

Korzystając z możliwych warunków, przyjrzyjmy się przykładowym automatyzacjom. Mamy poniżej przykład powiadomienia domowników:

  • o zbyt wysokim poziomie CO2 i konieczności przewietrzenia salonu
  • kiedy domowników nie ma w domu, a pojawi się wysoki dźwięk, który może sugerować np. włamanie bądź niszczenie domu przez psa
  • kiedy temperatura za oknem jest powyżej 15°C i ciągle rośnie, a domownik jest w na terenie domu, otrzyma sugestię o otwarciu okna
  • tutaj sytuacja odwrotna, kiedy temperatura na koniec dnia już spada, a okno jest otwarte, otrzymamy powiadomienie o jego zamknięciu i uniknięciu straty cennej energii cieplnej po nagrzaniu pokoju
  • kiedy wilgotność w salonie jest wyjątkowo wysoka powyżej 70%, dostaniemy sugestię o przewietrzeniu pokoju i uniknięciu powstania pleśni

Poniżej screeny z aplikacji SmartThings.

Pierwsze kroki i montaż

W pierwszej kolejności po wyjęciu urządzeń z pudełka. Podłączamy główną stację do zasilania oraz instalujemy baterie w module zewnętrznym. W tym celu musimy odkręcić dół sensora.

Po zamontowaniu zasilania możemy przejść do umieszczenia elementów stacji, główny moduł nie powinien znajdować się w zamkniętej przestrzeni (np. szafie) ani być narażonym na promienie słońca ani ciepło grzejnika. Moduł zewnętrzny również nie może być narażonym na promieniowanie słoneczne, powinien zostać umieszczony nad zadaszeniem oraz nie w pobliży wywietrzników domu. Pamiętajmy również o dobrym zamocowaniu, aby przy większym wietrze nie został uszkodzony. Zamontowanie w złym miejscu czujników spowoduje zafałszowanie jego pomiarów.

Konfiguracja w aplikacji Netatmo

Po umieszczeniu modułów stacji pogody, możemy przystąpić do konfiguracji aplikacji. Konieczne jest pobieranie bezpłatnej aplikacji „Netatmo Weather” z Google Play (Android 4.2 lub nowszy) bądź AppStore (iOS 12 lub nowszy). Po zainstalowaniu otwieramy aplikację. W pierwszej kolejności musimy się zalogować, bądź zarejestrować. Konieczne jest akceptowanie zgody na lokalizację smartfona. Łączymy stację pogody z naszą siecią Wi-Fi. Pod koniec konfiguracji zostaniemy zapytani czy wyrażamy zgodę na udostępnienie danych zewnętrznych z naszej domowej stacji meteorologicznej społeczności Netatmo, w celu współtworzenia map pogody. Na koniec ukaże się nam główny ekran aplikacji.

Zostając jeszcze chwilę w aplikacji Netatmo, mamy tutaj możliwość tworzenia pokoi. Po kliknięciu w menu po lewej stronie >> Manage my home >> My rooms (wybrany pokój) a następnie moduł stacji pogody. Ukażą nam się najważniejsze informacje o sygnale, baterii, wersji oprogramowania, ale również możliwość kalibracji pomiarów (Temperatury – Temperature, Wilgotności – Humidity, CO2, Ciśnienia atmosferycznego). Musimy jednak poczekać ok. 15-20 minut na aktualizację stanów z uwzględnieniem przesunięć po kalibracji.

  • UWAGA! Przed przystąpieniem do dodania stacji pogody do ekosystemu SmartThings zalecam utworzenie nowych pokoi oraz umieszczenie w nich poszczególnych modułów.

Instalacja w ekosystemie SmartThings

Niestety Netatmo Weather Station nie jest wspierane przez aplikację SmartThings. Na szczęście dzięki społeczności SmartThings powstał niestandardowy program obsługi urządzenia. W pierwszej kolejności musimy zarejestrować naszą aplikację na stronie Netatmo dla developerów: dev.netatmo.com. Na stronie logujemy się na nasze konto Neatatmo i szukamy „Register your App and start coding” i kliknij.

W formularzu wpisujemy tylko nazwę „app name”, opis aplikacji „description”, np. Smartthings connect oraz imię i osobistego maila, akceptujemy warunki użytkowania i naciskamy przycisk tworzenia „Create”.

Po nowo utworzonej aplikacji notujemy identyfikator klienta i secret, będziemy go później potrzebować na stronie IDE SmartThings. WAŻNE- Zwróć uwagę na status, aby był aktywny „Activated”.

Teraz możemy przejść do środowiska programistycznego SmartThings. Wchodzimy na stronę IDE i logujemy się na swoje konto Samsung. W pierwszej kolejności musimy stworzyć SmartApp, która połączy się z danymi z stacji pogody Netatmo. W tym celu kliknij „My SmartApps” >> „+ New SmartApp”.

Kliknij „From Code”, wklej poniższy kod z Github, a następnie kliknij „Create”. Kod został zaczerpnięty z GitHuba, szczegóły można zaleźć tutaj.

Kod SmartApps:

/**
 * Netatmo Connect
 */

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 "Showing the login page"
			return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
				section() {
					paragraph "Tap below to log in to Netatmo and authorize SmartThings access."
					href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description
				}
			}
		} else {
			log.debug "Showing the devices page"
			return dynamicPage(name: "Credentials", title: "Connected", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
				section() {
					input(name:"Devices", style:"embedded", required:false, title:"Netatmo 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 "OAuth flow succeeded"
	def message = """
	<p>We have located your """ + getVendorName() + """ account.</p>
	<p>Tap 'Done' to continue to Devices.</p>
	"""
	connectionStatus(message)
}

def fail() {
	log.debug "OAuth flow failed"
	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, DeviceID: ${deviceId} Device name: ${detail.module_name}"
					createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name)
					break
				case 'NAModule1':
					log.debug "Creating Outdoor module, DeviceID: ${deviceId} Device name: ${detail.module_name}"
					createChildDevice("Netatmo Outdoor Module", deviceId, "${detail.type}.${deviceId}", detail.module_name)
					break
				case 'NAModule3':
					log.debug "Creating Rain Gauge, DeviceID: ${deviceId} Device name: ${detail.module_name}"
					createChildDevice("Netatmo Rain", deviceId, "${detail.type}.${deviceId}", detail.module_name)
					break
				case 'NAModule4':
					log.debug "Creating Additional module, DeviceID: ${deviceId} Device name: ${detail.module_name}"
					createChildDevice("Netatmo Additional Module", deviceId, "${detail.type}.${deviceId}", detail.module_name)
					break
                case 'NAModule2':
					log.debug "Creating Wind module, DeviceID: ${deviceId} Device name: ${detail.module_name}"
					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) }

	// check if user has set location
    checkloc()
	// Do the initial poll
	poll()
	// Schedule it to run every 5 minutes
	//runEvery5Minutes("poll")
    
    // Schedule it to run every 2 minutes
    schedule("0 /2 * * * ?", 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 "Listing devices $devices "

	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', BFT:'BFT']
            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
            input title: "Version ID", description: "060221", type: "paragraph", element: "paragraph"
        }
	}
}

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}"
    //log.debug "Time Zone: ${location.timeZone}"
     

	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':
				if(data == null) {
                log.error "Main Module is missing data"
                } else {             
				log.debug "Updating Basestation $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: 'tempTrend', value: data['temp_trend'], unit: "")                
                child?.sendEvent(name: 'atmosphericPressure', value: (pressToPref(data['Pressure'])).toDouble().round(), unit: settings.pressUnits)
                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: 'pressureTrend', value: data['pressure_trend'], unit: "")
                child?.sendEvent(name: 'minTemp', value: cToPref(data['min_temp']) as float, unit: getTemperatureScale())
                child?.sendEvent(name: 'maxTemp', value: cToPref(data['max_temp']) as float, unit: getTemperatureScale())
                child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']), unit: "")
                child?.sendEvent(name: 'minTempTime', value: lastUpdated(data['date_min_temp']), unit: "")
                child?.sendEvent(name: 'maxTempTime', value: lastUpdated(data['date_max_temp']), unit: "")
				break;
                }
			case 'NAModule1':
				if(data == null) {
                log.error "Outdoor Module is missing data"
                } else {            
				log.debug "Updating Outdoor Module $data"
				child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
				child?.sendEvent(name: 'humidity', value: data['Humidity'], unit: "%")
                child?.sendEvent(name: 'tempTrend', value: data['temp_trend'], unit: "")
                child?.sendEvent(name: 'minTemp', value: cToPref(data['min_temp']) as float, unit: getTemperatureScale())
                child?.sendEvent(name: 'maxTemp', 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: 'minTempTime', value: lastUpdated(data['date_min_temp']), unit: "")
                child?.sendEvent(name: 'maxTempTime', value: lastUpdated(data['date_max_temp']), unit: "")
				break;
                }
			case 'NAModule3':
				if(data == null) {
                log.error "Rain Module is missing data"
                } else {            
				log.debug "Updating Rain Module $data"
				child?.sendEvent(name: 'rain', value: (rainToPref(data['Rain'])), unit: settings.rainUnits)
				child?.sendEvent(name: 'rainhour', value: (rainToPref(data['sum_rain_1'])), unit: settings.rainUnits)
				child?.sendEvent(name: 'rainday', value: (rainToPref(data['sum_rain_24'])), unit: settings.rainUnits)
                child?.sendEvent(name: 'battery', value: detail['battery_percent'], unit: "%")
                child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']))                
				break;
                }
			case 'NAModule4':
				if(data == null) {
                log.error "Additional module is missing data"
                } else {            
				log.debug "Updating Additional Module $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: 'tempTrend', value: data['temp_trend'], unit: "")                
                child?.sendEvent(name: 'minTemp', value: cToPref(data['min_temp']) as float, unit: getTemperatureScale())
                child?.sendEvent(name: 'maxTemp', 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: 'minTempTime', value: lastUpdated(data['date_min_temp']), unit: "")
                child?.sendEvent(name: 'maxTempTime', value: lastUpdated(data['date_max_temp']), unit: "")
				break;
                }
            case 'NAModule2':
				if(data == null) {
                log.error "Windmodule is missing data"
                } else {
                log.debug "Updating Wind Module $data"
				child?.sendEvent(name: 'wind', value: (windToPref(data['WindStrength'])).toDouble().trunc(1), unit: settings.windUnits)                
                child?.sendEvent(name: 'windAngle', value: data['WindAngle'], unit: "°")
                child?.sendEvent(name: 'windAngleText', value: windTotext(data['WindAngle']))
                child?.sendEvent(name: 'windDirection', value: windTotextonly(data['WindAngle']))
                child?.sendEvent(name: 'gustStrength', value: (windToPref(data['GustStrength'])).toDouble().trunc(1), unit: settings.windUnits)                
                child?.sendEvent(name: 'gustAngle', value: data['GustAngle'], unit: "°")
                child?.sendEvent(name: 'gustAngleText', value: gustTotext(data['GustAngle']))
                child?.sendEvent(name: 'gustDirection', value: windTotextonly(data['GustAngle']))
                child?.sendEvent(name: 'windMax', value: (windToPref(data['max_wind_str'])).toDouble().trunc(1), unit: settings.windUnits)
                child?.sendEvent(name: 'lastupdate', value: lastUpdated(data['time_utc']))
                child?.sendEvent(name: 'windMaxTime', value: lastUpdated(data['date_max_wind_str']))
                child?.sendEvent(name: 'battery', value: detail['battery_percent'], unit: "%")                           
                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
    } else if (settings.windUnits == 'BFT' && Wind < 1) {
    	return 0
    } else if (settings.windUnits == 'BFT' && Wind < 6) {
    	return 1
    } else if (settings.windUnits == 'BFT' && Wind < 12) {
    	return 2
    } else if (settings.windUnits == 'BFT' && Wind < 20) {
    	return 3
    } else if (settings.windUnits == 'BFT' && Wind < 29) {
    	return 4
    } else if (settings.windUnits == 'BFT' && Wind < 39) {
    	return 5
    } else if (settings.windUnits == 'BFT' && Wind < 50) {
    	return 6
    } else if (settings.windUnits == 'BFT' && Wind < 62) {
    	return 7
    } else if (settings.windUnits == 'BFT' && Wind < 75) {
    	return 8
    } else if (settings.windUnits == 'BFT' && Wind < 89) {
    	return 9
    } else if (settings.windUnits == 'BFT' && Wind < 103) {
    	return 10
    } else if (settings.windUnits == 'BFT' && Wind < 118) {
    	return 11
    } else if (settings.windUnits == 'BFT' && Wind >= 118) {
    	return 12
    }
}

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(location.timeZone == null) {
    log.warn "Time Zone is not set, time will be in UTC. Go to your ST app and set your hub location to get local time!"    
    	def updtTime = new Date(time*1000L).format("HH:mm")
    	state.lastUpdated = updtTime
    return updtTime + " UTC"   
    } else if(settings.time == '24') {
    	def updtTime = new Date(time*1000L).format("HH:mm", location.timeZone)
    	state.lastUpdated = updtTime
    return updtTime
    } else if(settings.time == '12') {
    	def updtTime = new Date(time*1000L).format("h:mm aa", location.timeZone)
    	state.lastUpdated = updtTime
    return updtTime
    }
}

def windTotext(WindAngle) {
	if(WindAngle < 0) { 
    	return "No Wind"
    } else if (WindAngle < 23) {
    	return WindAngle + "° North"        
    } else if (WindAngle < 68) {
    	return WindAngle + "° NEast"
    } else if (WindAngle < 113) {
    	return WindAngle + "° East"
    } else if (WindAngle < 158) {
    	return WindAngle + "° SEast"
    } else if (WindAngle < 203) {
    	return WindAngle + "° South"
    } else if (WindAngle < 248) {
    	return WindAngle + "° SWest"
    } else if (WindAngle < 293) {
    	return WindAngle + "° West"
    } else if (WindAngle < 338) {
    	return WindAngle + "° NWest"
    } 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 windTotextonly(WindAngle) {
	if(WindAngle < 0) { 
    	return "No Wind"
    } else if (WindAngle < 23) {
    	return "North"        
    } else if (WindAngle < 68) {
    	return "NEast"
    } else if (WindAngle < 113) {
    	return "East"
    } else if (WindAngle < 158) {
    	return "SEast"
    } else if (WindAngle < 203) {
    	return "South"
    } else if (WindAngle < 248) {
    	return "SWest"
    } else if (WindAngle < 293) {
    	return "West"
    } else if (WindAngle < 338) {
    	return "NWest"
    } else if (WindAngle < 361) {
    	return "North"
    }
}

def gustTotextonly(GustAngle) {
	if(GustAngle < 23) { 
    	return "North"
    } else if (GustAngle < 68) {
    	return "NEast"
    } else if (GustAngle < 113) {
    	return "East"
    } else if (GustAngle < 158) {
    	return "SEast"
    } else if (GustAngle < 203) {
    	return "South"
    } else if (GustAngle < 248) {
    	return "SWest"
    } else if (GustAngle < 293) {
    	return "West"
    } else if (GustAngle < 338) {
    	return "NWest"
    } else if (GustAngle < 361) {
    	return "North"
    }
}

def noiseTosound(Noise) {
	if(Noise > settings.sound) { 
    	return "detected"
    } else {
    	return "not detected"
    }
}

def checkloc() {

    if(location.timeZone == null)
		sendPush("Netatmo: Time Zone is not set, time will be in UTC. Go to your ST app and set your hub location to get local time!")
}        
        

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

Po skopiowaniu kodu, kliknij „Publish” >> „For Me”. Po opublikowniu SmartApp, konieczna jest zmiana jej ustawień. Klikamy „App Settings”.

Na następnej stronie rozwijamy sekcje „Settings”. Wpisujemy zapisane „ClientID” oraz „Secret”, które otrzymaliśmy wcześniej podczas tworzenia aplikacji na Netatmo DEV.
Należy również uzupełnić „serverUrl”, którego ja użyłem: https://graph-eu01-euwest1.api.smartthings.com/ . Mamy do dyspozycji również:

Po uzupełnieniu danych, kliknij sekcję „OAuth” i włącz go. Zapisujemy zamiany klikając „Update”.

Po zainstalowaniu SmartApp możemy przejść do instalacji niestandardowych programów obsługi dla modułów stacji pogody. Wracamy do sekcji „My Device Handlers”, i tworzymy nowy program „+ Create New Device Handler” w prawym górnym rogu, klikamy „From Code”. Szczegółową instrukcję instalacji niestandardowego programu obsługi wraz ze screenami można znaleźć we wcześniejszym wpisie.

Wklejamy w okno, poniższe kody obsługi. Później klikamy „Create” >> „Publish” >> „For Me”. Musimy powtórzyć dodawanie programu obsługi dla każdego z modułów stacji pogody Netatmo. Kod został zaczerpnięty z GitHuba, szczegóły można zaleźć tutaj.

Kod obsługi głównego modułu stacji pogody Netatmo:

/**
 *  Netatmo Basestation
 *
 *  Copyright 2019 cscheiene
 *
 *  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.
 */
metadata {
	definition (name: "Netatmo Basestation", namespace: "cscheiene", author: "cscheiene", ocfDeviceType: "oic.d.thermostat", mnmn: "SmartThingsCommunity", vid: "f151e292-0096-3813-81ce-bf6965d4572d") {
		capability "Temperature Measurement"
        capability "Sensor"
        capability "Carbon Dioxide Measurement"
		capability "Relative Humidity Measurement"
		capability "Sound Pressure Level"
		capability "Sound Sensor"		
        capability "Refresh"
        capability "Health Check"
        capability 'Atmospheric Pressure Measurement'
        capability "islandtravel33177.lastUpdate"
        capability "islandtravel33177.tempTrend"
        capability "islandtravel33177.minTemp"
        capability "islandtravel33177.maxTemp"
        capability "islandtravel33177.minTempTime"
        capability "islandtravel33177.maxTempTime"
        capability "islandtravel33177.pressureTrend"
	}

	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"
        input title: "Version ID", description: "060221", displayDuringSetup: false, type: "paragraph", element: "paragraph"
    }      
}

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

def installed() {
	sendEvent(name: "checkInterval", value: 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "cloud"])
    sendEvent(name: 'battery', value: 100, unit: "%") //workaround for the new ST app
}

def updated() {
	sendEvent(name: "checkInterval", value: 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "cloud"])
    sendEvent(name: 'battery', value: 100, unit: "%") //workaround for the new ST app
}        

Kod obsługi modułu zewnętrznego stacji pogody Netatmo:

/**
 *  Netatmo Outdoor Module
 *
 *  Copyright 2019 cscheiene
 *
 *  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.
 */
metadata {
	definition (name: "Netatmo Outdoor Module", namespace: "cscheiene", author: "cscheiene", ocfDeviceType: "oic.d.thermostat", mnmn: "SmartThingsCommunity", vid: "d8cf0a09-adc4-3411-91f5-c7f3250c274f") {
		capability "Sensor"
        capability "Health Check"
        capability "Battery"
		capability "Relative Humidity Measurement"
		capability "Temperature Measurement"
        capability "islandtravel33177.lastUpdate"
        capability "islandtravel33177.tempTrend"
        capability "islandtravel33177.minTemp"
        capability "islandtravel33177.maxTemp"
        capability "islandtravel33177.minTempTime"
        capability "islandtravel33177.maxTempTime"
	}

	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"
        input title: "Version ID", description: "060221", displayDuringSetup: false, type: "paragraph", element: "paragraph"
    }  
}

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

def installed() {
	sendEvent(name: "checkInterval", value: 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "cloud"])
}

def updated() {
	sendEvent(name: "checkInterval", value: 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "cloud"])
}

Dostępne są również kody dla modułu wiatromierza oraz deszczomierza. Zalecam utworzenie już wszystkich kodów, przy późniejszym wzbogacaniu stacji o nowe moduły, nie będzie konieczne kopiowanie kodów. Pierwotną instrukcje obsługi i dokładny opis programu obsługi, sporządzony przez developera, można znaleźć na oficjalnym forum SmartThings – tutaj.

UWAGA! Przed przystąpieniem do dodania stacji pogody do ekosystemu SmartThings zalecam utworzenie nowych pokoi oraz umieszczenie w nich po jednym z poszczególnych modułów.

Aby zainstalować Netatmo Weather Station w ekosystemie SmartThings należy na ekranie głównym mobilnej aplikacji SmartThings rozwinąć lewe menu. Kolejno „SmartApps” i przycisk dodaj „+”. Teraz wybieramy z pośród listy „Netatmo (Connect) Modified”. Autoryzujemy połączenie z kontem Netatmo. Klikamy „Click to enter Credentials”, zostaniemy przeniesieni do strony Netatmo, gdzie logujemy się na swoje konto i akceptujemy dostęp SmartThings do danych. Na zakończenie klikamy „Gotowe”. Po zamknięciu strony, klikamy „Dalej”. Ukaże się nam panel konfiguracji SmartApps. Wybieramy moduły naszej nowej stacji pogody oraz poszczególne jednostki dla parametrów (Rain, Pressure, Wind, Time Format). Po kliknięciu na dole „Gotowe”, wszystkie moduły ukażą się w sekcji „Brak przypisanego pokoju” na dole głównego ekranu aplikacji SmartThings.

Wchodząc ponownie w SmartApps Netatmo (Connect) możemy zmienić moduły oraz jednostki parametrów.

Jeśli pojawią się problemy i moduły stacji pogody nie zostaną prawidłowo dodane konieczna będzie ręczna zmiana sterownika w „SmartThings Groovy IDE”. W tym celu wchodzimy na stronę -( link ) i logujemy się na swoje konto Samsung.

Kolejno wchodzimy w „My Device” i odnajdujemy moduł, klikamy nazwę i przechodzimy do szczegółów urządzenia, na końcu strony i klikamy „Edit”.

Tutaj musimy zmienić typ (program sterowania) modułu, klikamy „Type” i rozwijamy listę sterowników. Na dole rozwiniętej listy powinien znajdować się nasz niestandardowy program obsługi, wybieramy odpowiedni dla naszego modułu. Po czym klikamy „Update”, po chwili urządzenie powinno zacząć prawidłowo funkcjonować z aplikacją SmartThings.

Gdzie zakupić ?

Stacja pogody Netatmo Weather Station jest dostępna w różnych zestawach, różniącymi się modułami w opakowaniu. Podstawowy zestaw z jednym modułem pokojowym oraz jednym zewnętrznym to koszt 799 zł. Dodatkowy moduł wewnętrzy to koszt 329 zł. Wiatromierz do stacji pogody kosztuje 469 zł, a deszczomierz 329 zł.

W podstawowym zestawie w pudełku znajdziemy główną stację pogody z zasilaczem, moduł zewnętrzny z bateriami. Dostajemy dodatkowo śrubę i pasek do zamontowania zewnętrznej stacji. Znajduje się również instrukcja montażu oraz kod HomeKit w celu instalacji w ekosystemie Apple.

3 myśli w temacie “Netatmo Weather Station – prywatna stacja pogody dla SmartThings

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s