2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.meater.internal.api;
15 import java.util.List;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeoutException;
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;
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;
48 * The {@link MeaterRestAPI} class defines the MEATER REST API
50 * @author Jan Gustafsson - Initial contribution
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;
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;
67 public MeaterRestAPI(MeaterBridgeConfiguration configuration, Gson gson, HttpClient httpClient,
68 LocaleProvider localeProvider) {
70 this.configuration = configuration;
71 this.httpClient = httpClient;
72 this.localeProvider = localeProvider;
75 public boolean refresh(Map<String, MeaterProbeDTO.Device> meaterProbeThings) {
77 MeaterProbeDTO dto = getDevices(MeaterProbeDTO.class);
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);
86 meaterProbeThings.clear();
91 } catch (MeaterException e) {
92 logger.warn("Failed to refresh! {}", e.getMessage());
97 private void login() throws MeaterException {
100 String json = "{ \"email\": \"" + configuration.email + "\", \"password\": \"" + configuration.password
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);
107 logger.trace("{}.", request.toString());
109 ContentResponse httpResponse = request.send();
110 if (!HttpStatus.isSuccess(httpResponse.getStatus())) {
111 throw new MeaterException("Failed to login " + httpResponse.getContentAsString());
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();
121 throw new MeaterException("Token is not present in the JSON response");
123 } catch (TimeoutException | ExecutionException | JsonParseException e) {
124 throw new MeaterException(e);
125 } catch (InterruptedException e) {
126 Thread.currentThread().interrupt();
127 throw new MeaterException(e);
131 private String getFromApi(String uri) throws MeaterException {
133 for (int i = 0; i < MAX_RETRIES; i++) {
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());
141 ContentResponse response = request.send();
142 String content = response.getContentAsString();
143 logger.trace("API response: {}", content);
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!");
155 } catch (TimeoutException e) {
156 logger.debug("TimeoutException error in get: {}", e.getMessage());
159 throw new MeaterException("Failed to fetch from API!");
160 } catch (ExecutionException e) {
161 Throwable cause = e.getCause();
162 if (cause instanceof HttpResponseException httpResponseException) {
163 Response response = httpResponseException.getResponse();
164 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
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
171 logger.debug("getFromApi failed, authentication failed, HTTP status: 401");
172 throw new MeaterAuthenticationException("Authentication failed");
175 throw new MeaterException(e);
176 } catch (InterruptedException e) {
177 Thread.currentThread().interrupt();
178 throw new MeaterException(e);
182 public @Nullable <T> T getDevices(Class<T> dto) throws MeaterException {
183 String uri = DEVICES;
186 if (authToken.isEmpty()) {
191 json = getFromApi(uri);
192 } catch (MeaterAuthenticationException e) {
193 logger.debug("getFromApi failed {}", e.getMessage());
196 json = getFromApi(uri);
199 if (json.isEmpty()) {
200 throw new MeaterException("JSON from API is empty!");
203 return gson.fromJson(json, dto);
204 } catch (JsonSyntaxException e) {
205 throw new MeaterException("Error parsing JSON", e);