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