Spring Boot Applikation - Netatmo Service mit REST

16.05.2016

Spring Boot - Netatmo Service mit REST

Dieser Artikel beschreibt, wie man die Wetterdaten einer Wetterstation von Netatmo über die REST-Schnittstelle des Netatmo-Server abfragt. Zur Authentifizierung wird dabei OAuth 2.0 verwendet. In dem Artikel wird zuerst der notwendige Programmcode gezeigt, um die Werte über die REST-API abzufragen. Anschliessend wird ein kleiner Webservice mit Spring Boot und einer H2-Datenbank erstellt, der wiederum eine lokale REST-Schnittstelle anbietet, aber die Wetterdaten aus der lokalen Datenbank bezieht.

Der Grund für diese Lösung ist, dass dieser 'Microservice' um weitere Microservices erweitert werden soll und durch die zusätzliche lokale Datenhaltung robuster ist und zumindest immer den letzten Temperaturwert liefert auch wenn der Netatmo-Server nicht verfügbar ist.

Voraussetzung ist, dass man sich im Developer-Bereich auf der Webseite von Netatmo einen Account angelegt hat und anschliessend unter (Create App) ein paar Client-Credentials erstellt, um sich später über OAuth 2.0 bei Netatmo zu authentifizieren.

Nachdem man die Client-Credentials hat, können diese in der folgenden Methode eingesetzt werden, um einen Access- und Refresh-Token vom Server zu erhalten. Die URL, die aufgerufen werden muss ist folgende: https://api.netatmo.net/oauth2/token

Als Parameter werden die Client-Credentials in einer MultiValueMap an den Server übertragen. Bei dem Aufruf handelt es sich um einen einfachen REST-Call, der mittels des RestTemplate org.springframework.web.client.RestTemplate des Spring-Frameworks gemacht wird.

    public  Token getNetatmoToken() {
        MultiValueMap<String, String> mvm = new LinkedMultiValueMap<>();
        mvm.add("client_id", client_id);
        mvm.add("client_secret", client_secret);
        mvm.add("grant_type", "password");
        mvm.add("username", username);
        mvm.add("password", password);
        mvm.add("scope", "read_station read_thermostat write_thermostat");

        return new RestTemplate().postForObject("https://api.netatmo.net/oauth2/token", mvm, Token.class);
    }

Die Rückgabewerte werden in einer Klasse Token gespeichert und enthalten nur den Access- und Refresh-Token.

public class Token {

    private String access_token;
    private long expires_in;
    private String  refresh_token;
    public String getAccess_token() {
        return access_token;
    }
    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }
    public long getExpires_in() {
        return expires_in;
    }
    public void setExpires_in(long expires_in) {
        this.expires_in = expires_in;
    }
    public String getRefresh_token() {
        return refresh_token;
    }
    public void setRefresh_token(String refresh_token) {
        this.refresh_token = refresh_token;
    }

}

Mit den Token lassen sich nun Werte über die REST-Schnittstelle von Netatmo abfragen. Im folgenden werden nur die aktuellen Werte der Wetterstation abgefragt, die auch in der iOS-App angzeigt werden. (Dashboard).

    private void readNetatmoTemperature(Token token) throws IOException {
        ResponseEntity<String> resp = new RestTemplate().getForEntity("https://api.netatmo.net/api/devicelist?access_token=" + token.getAccess_token(), String.class);

        String json = resp.getBody();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jsonNode = mapper.readTree(json);
        JsonNode devices = jsonNode.findValue("devices");
        JsonNode dashboard = devices.findValue("dashboard_data");
        JsonNode temperature = dashboard.findValue("Temperature");
        JsonNode time = dashboard.findValue("time_utc");

        Temperatur temperatur = new Temperatur();
        temperatur.setDate(new Date(time.asLong() * 1000));
        temperatur.setTemperatur(temperature.asText());
    }

In dem Beispiel werden die Werte in einer Klasse Temperatur gespeichert. Zum Parsen der JSON-Response wird das Framework Jackson verwendet. Die Fehlerbehandlung fehlt in diesem Code-Schnipsel, ist aber weiter unter in der Klasse TemperaturService zu finden.

/**
 * Created by hameister on 14.05.16.
 */
@Entity
@Table(name = "Temperatur")
public class Temperatur {

    @Id
    @GeneratedValue
    Long id;

    @Column(name = "temperatur")
    String temperatur;

    @Column(name = "date")
    Date date;

    public Temperatur() {
    }


    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getTemperatur() {
        return temperatur;
    }

    public void setTemperatur(String temperatur) {
        this.temperatur = temperatur;
    }
}

Prinzipiell kann man mit diesem Quellcode die Temperaturwerte abfragen. In der JSON-Response findet man auch alle registrierten Module. Die in der Vergangenheit gemessenen Werte lassen sich über die REST-Schnittstelle https://api.netatmo.com/api/getmeasure abfragen. Genaueres dazu findet man in der API-Dokumentation.

Hier wird nun beschrieben, wie man eine Spring Boot-Applikation erstellt, die die Dashboard-Werte bei Netatmo abfragt und in einer Datenbank speichert.

Der einfachste Weg eine Spring Boot-Applikation zu erstellen, ist auf der Webseite start.spring.io zu starten.

Dort wählt man H2, Web und JPA aus und erstellt mit Generate Project ein Maven-Projekt mit allen benötigten Abhängigkeiten.

Das Projekt öffnet man nun als Maven-Projekt in einer IDE. Hier wird IntelliJ verwendet.

Anschliessend wird folgende Paketstruktur mit den abgebildeten Klassen angelegt.

Das Interface TemperaturRepository enthält folgendes:

@Repository
public interface TemperaturRepository extends JpaRepository<Temperatur, Long> {
}

Als nächstes wird der Code im TemperaturService ergänzt:

package org.hameister.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.log4j.spi.LoggerFactory;
import org.hameister.model.Temperatur;
import org.hameister.model.Token;
import org.hameister.repository.TemperaturRepository;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.Date;
import java.util.List;

/**
 * Created by hameister on 14.05.16.
 */
@Service
public class TemperaturService {

    public Logger logger = (Logger) org.slf4j.LoggerFactory.getLogger(TemperaturService.class);

    @Value("${client_id}")
    String client_id;
    @Value("${client_secret}")
    String client_secret;
    @Value("${username}")
    String username;
    @Value("${password}")
    String password;

    @Autowired
    TemperaturRepository repository;

    private RestTemplate restTemplate;

    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public Temperatur getTemperatur() {
        try {
            readNetatmoTemperature(getNetatmoToken());
        } finally {
            List<Temperatur> temperatures = repository.findAll();
            if( temperatures.size()>0) {;
                return temperatures.get(temperatures.size() - 1);
            }
            else {
                // If there is no old value
                return  new Temperatur();
            }
        }
    }


    private void readNetatmoTemperature(Token token) throws  RestClientException{
        ResponseEntity<String> resp =restTemplate.getForEntity("https://api.netatmo.net/api/devicelist?access_token=" + token.getAccess_token(), String.class);

        String json = resp.getBody();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jsonNode = null;
        try {
            jsonNode = mapper.readTree(json);
        } catch (IOException e) {
            logger.error("Invalid JSON from Server.");

        }
        JsonNode devices = jsonNode.findValue("devices");
        JsonNode dashboard = devices.findValue("dashboard_data");
        JsonNode temperature = dashboard.findValue("Temperature");
        JsonNode time = dashboard.findValue("time_utc");

        Temperatur temperatur = new Temperatur();
        temperatur.setDate(new Date(time.asLong() * 1000));
        temperatur.setTemperatur(temperature.asText());
        repository.save(temperatur);
        System.out.println("Temperatur:" + temperature.asDouble());
    }

    protected Token getNetatmoToken() throws RestClientException {
        if(restTemplate == null) {
            restTemplate = new RestTemplate();
        }

        MultiValueMap<String, String> mvm = new LinkedMultiValueMap<>();
        mvm.add("client_id", client_id);
        mvm.add("client_secret", client_secret);
        mvm.add("grant_type", "password");
        mvm.add("username", username);
        mvm.add("password", password);
        mvm.add("scope", "read_station read_thermostat write_thermostat");

        return restTemplate.postForObject("https://api.netatmo.net/oauth2/token", mvm, Token.class);

    }
}

Zu beachten ist, dass die Werte für client_id, usw. aus der Datei application.properties gelesen werden. Hier müssen die entsprechenden Credentials, die man von Netatmo erhalten hat, eingetragen werden. Zu beachten ist hier, dass auch die H2-Datenbank konfiguriert wird.

spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.DefaultNamingStrategy
spring.jpa.hibernate.ddl-auto=update

spring.datasource.url=jdbc:h2:file:./temperatureDB
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.driver-class-name=org.h2.Driver

#Client-Credentials
client_id=hlkjhkjlhlkj
client_secret=lgkgjh687687gj
username=donald
password=hkhkjh/h

Jetzt fehlt noch der TemperaturController, damit man über eine REST-Schnittstelle die Temperatur lokal abfragen kann.

package org.hameister.controller;

import org.hameister.model.Temperatur;
import org.hameister.service.TemperaturService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by hameister on 14.05.16.
 */
@RestController
public class TemperaturController {

    @Autowired
    TemperaturService temperaturService;

    /**************
     Temperature
     **************/
    @RequestMapping(value = "/api/temperature", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    ResponseEntity<String> getTemperature() {
        Temperatur temperatur = temperaturService.getTemperatur();

        return new ResponseEntity<String>(temperatur.getTemperatur(), HttpStatus.OK);
    }

}

Nun kann man die Spring Boot-Applikation mit mvn spring-boot:run starten und anschliessend durch den Aufruf folgender URL die Temperatur abfragen: Temperatur

Der über den TemperaturController abgefragte Wert wurde vor dem Anzeigen durch den TemperaturService in der Datenbank gespeichert, so dass auch ein Wert angezeigt wird, wenn keine Verbindung zu Netatmo hergestellt werden kann. In der Beispielanwendung wird bei einer fehlerhaften Verbindung zum Server der letzte Wert aus der Datenbank zurückgeliefert. Wenn noch beim ersten Start noch keine Datenbank existiert, wird ein Leerstring zurückgegeben.

Ausführlichere Erklärungen zu der REST-Schnittstelle und der Datenbankanbindung mit JPA findet man in dem Artikel Spring Boot: Ein Beispiel mit JPA und H2 und Spring Boot: Ein Beispiel mit JPA und H2 DB und REST-Schnittstelle.

Der komplette Quellcode ist bei GitHub unter folgender URL zu finden: SpringBootNetatmoService