]> git.basschouten.com Git - openhab-addons.git/blob
41a2cb64d5d346d0622864ce1deb7bf953de2f1e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.meater.internal.api;
14
15 import java.util.List;
16 import java.util.Map;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeoutException;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.eclipse.jetty.client.HttpClient;
23 import org.eclipse.jetty.client.HttpResponseException;
24 import org.eclipse.jetty.client.api.ContentResponse;
25 import org.eclipse.jetty.client.api.Request;
26 import org.eclipse.jetty.client.api.Response;
27 import org.eclipse.jetty.client.util.StringContentProvider;
28 import org.eclipse.jetty.http.HttpHeader;
29 import org.eclipse.jetty.http.HttpMethod;
30 import org.eclipse.jetty.http.HttpStatus;
31 import org.openhab.binding.meater.internal.MeaterBridgeConfiguration;
32 import org.openhab.binding.meater.internal.dto.MeaterProbeDTO;
33 import org.openhab.binding.meater.internal.dto.MeaterProbeDTO.Device;
34 import org.openhab.binding.meater.internal.exceptions.MeaterAuthenticationException;
35 import org.openhab.binding.meater.internal.exceptions.MeaterException;
36 import org.openhab.core.i18n.LocaleProvider;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.google.gson.Gson;
41 import com.google.gson.JsonElement;
42 import com.google.gson.JsonObject;
43 import com.google.gson.JsonParseException;
44 import com.google.gson.JsonParser;
45 import com.google.gson.JsonSyntaxException;
46
47 /**
48  * The {@link MeaterRestAPI} class defines the MEATER REST API
49  *
50  * @author Jan Gustafsson - Initial contribution
51  */
52 @NonNullByDefault
53 public class MeaterRestAPI {
54     private static final String API_ENDPOINT = "https://public-api.cloud.meater.com/v1/";
55     private static final String JSON_CONTENT_TYPE = "application/json";
56     private static final String LOGIN = "login";
57     private static final String DEVICES = "devices";
58     private static final int MAX_RETRIES = 3;
59
60     private final Logger logger = LoggerFactory.getLogger(MeaterRestAPI.class);
61     private final Gson gson;
62     private final HttpClient httpClient;
63     private final MeaterBridgeConfiguration configuration;
64     private String authToken = "";
65     private LocaleProvider localeProvider;
66
67     public MeaterRestAPI(MeaterBridgeConfiguration configuration, Gson gson, HttpClient httpClient,
68             LocaleProvider localeProvider) {
69         this.gson = gson;
70         this.configuration = configuration;
71         this.httpClient = httpClient;
72         this.localeProvider = localeProvider;
73     }
74
75     public boolean refresh(Map<String, MeaterProbeDTO.Device> meaterProbeThings) {
76         try {
77             MeaterProbeDTO dto = getDevices(MeaterProbeDTO.class);
78             if (dto != null) {
79                 List<Device> devices = dto.getData().getDevices();
80                 if (devices != null) {
81                     if (!devices.isEmpty()) {
82                         for (Device meaterProbe : devices) {
83                             meaterProbeThings.put(meaterProbe.id, meaterProbe);
84                         }
85                     } else {
86                         meaterProbeThings.clear();
87                     }
88                     return true;
89                 }
90             }
91         } catch (MeaterException e) {
92             logger.warn("Failed to refresh! {}", e.getMessage());
93         }
94         return false;
95     }
96
97     private void login() throws MeaterException {
98         try {
99             // Login
100             String json = "{ \"email\": \"" + configuration.email + "\",  \"password\": \"" + configuration.password
101                     + "\" }";
102             Request request = httpClient.newRequest(API_ENDPOINT + LOGIN).method(HttpMethod.POST);
103             request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
104             request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
105             request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
106
107             logger.trace("{}.", request.toString());
108
109             ContentResponse httpResponse = request.send();
110             if (!HttpStatus.isSuccess(httpResponse.getStatus())) {
111                 throw new MeaterException("Failed to login " + httpResponse.getContentAsString());
112             }
113             // Fetch JWT
114             json = httpResponse.getContentAsString();
115             JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
116             JsonObject childObject = jsonObject.getAsJsonObject("data");
117             JsonElement tokenJson = childObject.get("token");
118             if (tokenJson != null) {
119                 this.authToken = tokenJson.getAsString();
120             } else {
121                 throw new MeaterException("Token is not present in the JSON response");
122             }
123         } catch (TimeoutException | ExecutionException | JsonParseException e) {
124             throw new MeaterException(e);
125         } catch (InterruptedException e) {
126             Thread.currentThread().interrupt();
127             throw new MeaterException(e);
128         }
129     }
130
131     private String getFromApi(String uri) throws MeaterException {
132         try {
133             for (int i = 0; i < MAX_RETRIES; i++) {
134                 try {
135                     Request request = httpClient.newRequest(API_ENDPOINT + uri).method(HttpMethod.GET);
136                     request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
137                     request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
138                     request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
139                     request.header(HttpHeader.ACCEPT_LANGUAGE, localeProvider.getLocale().getLanguage());
140
141                     ContentResponse response = request.send();
142                     String content = response.getContentAsString();
143                     logger.trace("API response: {}", content);
144
145                     if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
146                         // This will currently not happen because "WWW-Authenticate" header is missing; see below.
147                         logger.debug("getFromApi failed, authentication failed, HTTP status: 401");
148                         throw new MeaterAuthenticationException("Authentication failed");
149                     } else if (!HttpStatus.isSuccess(response.getStatus())) {
150                         logger.debug("getFromApi failed, HTTP status: {}", response.getStatus());
151                         throw new MeaterException("Failed to fetch from API!");
152                     } else {
153                         return content;
154                     }
155                 } catch (TimeoutException e) {
156                     logger.debug("TimeoutException error in get: {}", e.getMessage());
157                 }
158             }
159             throw new MeaterException("Failed to fetch from API!");
160         } catch (ExecutionException e) {
161             Throwable cause = e.getCause();
162             if (cause != null && cause instanceof HttpResponseException) {
163                 Response response = ((HttpResponseException) cause).getResponse();
164                 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
165                     /*
166                      * When contextId is not valid, the service will respond with HTTP code 401 without
167                      * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
168                      * HttpResponseException. We need to handle this in order to attempt
169                      * reauthentication.
170                      */
171                     logger.debug("getFromApi failed, authentication failed, HTTP status: 401");
172                     throw new MeaterAuthenticationException("Authentication failed");
173                 }
174             }
175             throw new MeaterException(e);
176         } catch (InterruptedException e) {
177             Thread.currentThread().interrupt();
178             throw new MeaterException(e);
179         }
180     }
181
182     public @Nullable <T> T getDevices(Class<T> dto) throws MeaterException {
183         String uri = DEVICES;
184         String json = "";
185
186         if (authToken.isEmpty()) {
187             login();
188         }
189
190         try {
191             json = getFromApi(uri);
192         } catch (MeaterAuthenticationException e) {
193             logger.debug("getFromApi failed {}", e.getMessage());
194             this.authToken = "";
195             login();
196             json = getFromApi(uri);
197         }
198
199         if (json.isEmpty()) {
200             throw new MeaterException("JSON from API is empty!");
201         } else {
202             try {
203                 return gson.fromJson(json, dto);
204             } catch (JsonSyntaxException e) {
205                 throw new MeaterException("Error parsing JSON", e);
206             }
207         }
208     }
209 }