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.electroluxair.internal.api;
15 import java.time.Instant;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeoutException;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jetty.client.HttpClient;
22 import org.eclipse.jetty.client.api.ContentResponse;
23 import org.eclipse.jetty.client.api.Request;
24 import org.eclipse.jetty.client.util.StringContentProvider;
25 import org.eclipse.jetty.http.HttpHeader;
26 import org.eclipse.jetty.http.HttpMethod;
27 import org.eclipse.jetty.http.HttpStatus;
28 import org.openhab.binding.electroluxair.internal.ElectroluxAirBridgeConfiguration;
29 import org.openhab.binding.electroluxair.internal.ElectroluxAirException;
30 import org.openhab.binding.electroluxair.internal.dto.ElectroluxPureA9DTO;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
34 import com.google.gson.Gson;
35 import com.google.gson.JsonObject;
36 import com.google.gson.JsonParser;
37 import com.google.gson.JsonSyntaxException;
40 * The {@link ElectroluxDeltaAPI} class defines the Elextrolux Delta API
42 * @author Jan Gustafsson - Initial contribution
45 public class ElectroluxDeltaAPI {
46 private static final String CLIENT_ID = "ElxOneApp";
47 private static final String CLIENT_SECRET = "8UKrsKD7jH9zvTV7rz5HeCLkit67Mmj68FvRVTlYygwJYy4dW6KF2cVLPKeWzUQUd6KJMtTifFf4NkDnjI7ZLdfnwcPtTSNtYvbP7OzEkmQD9IjhMOf5e1zeAQYtt2yN";
48 private static final String X_API_KEY = "2AMqwEV5MqVhTKrRCyYfVF8gmKrd2rAmp7cUsfky";
50 private static final String BASE_URL = "https://api.ocp.electrolux.one";
51 private static final String TOKEN_URL = BASE_URL + "/one-account-authorization/api/v1/token";
52 private static final String AUTHENTICATION_URL = BASE_URL + "/one-account-authentication/api/v1/authenticate";
53 private static final String API_URL = BASE_URL + "/appliance/api/v2";
54 private static final String APPLIANCES_URL = API_URL + "/appliances";
56 private static final String JSON_CONTENT_TYPE = "application/json";
57 private static final int MAX_RETRIES = 3;
59 private final Logger logger = LoggerFactory.getLogger(ElectroluxDeltaAPI.class);
60 private final Gson gson;
61 private final HttpClient httpClient;
62 private final ElectroluxAirBridgeConfiguration configuration;
63 private String authToken = "";
64 private Instant tokenExpiry = Instant.MIN;
66 public ElectroluxDeltaAPI(ElectroluxAirBridgeConfiguration configuration, Gson gson, HttpClient httpClient) {
68 this.configuration = configuration;
69 this.httpClient = httpClient;
72 public boolean refresh(Map<String, ElectroluxPureA9DTO> electroluxAirThings) {
74 if (Instant.now().isAfter(this.tokenExpiry)) {
75 // Login again since token is expired
79 String json = getAppliances();
80 ElectroluxPureA9DTO[] dtos = gson.fromJson(json, ElectroluxPureA9DTO[].class);
82 for (ElectroluxPureA9DTO dto : dtos) {
83 String applianceId = dto.getApplianceId();
85 String jsonApplianceInfo = getAppliancesInfo(applianceId);
86 ElectroluxPureA9DTO.ApplianceInfo applianceInfo = gson.fromJson(jsonApplianceInfo,
87 ElectroluxPureA9DTO.ApplianceInfo.class);
88 if (applianceInfo != null) {
89 if ("AIR_PURIFIER".equals(applianceInfo.getDeviceType())) {
90 dto.setApplianceInfo(applianceInfo);
91 electroluxAirThings.put(dto.getProperties().getReported().getDeviceId(), dto);
97 } catch (JsonSyntaxException | ElectroluxAirException e) {
98 logger.warn("Failed to refresh! {}", e.getMessage());
104 public boolean workModePowerOff(String applianceId) {
105 String commandJSON = "{ \"WorkMode\": \"PowerOff\" }";
107 return sendCommand(commandJSON, applianceId);
108 } catch (ElectroluxAirException e) {
109 logger.warn("Work mode powerOff failed {}", e.getMessage());
114 public boolean workModeAuto(String applianceId) {
115 String commandJSON = "{ \"WorkMode\": \"Auto\" }";
117 return sendCommand(commandJSON, applianceId);
118 } catch (ElectroluxAirException e) {
119 logger.warn("Work mode auto failed {}", e.getMessage());
124 public boolean workModeManual(String applianceId) {
125 String commandJSON = "{ \"WorkMode\": \"Manual\" }";
127 return sendCommand(commandJSON, applianceId);
128 } catch (ElectroluxAirException e) {
129 logger.warn("Work mode manual failed {}", e.getMessage());
134 public boolean setFanSpeedLevel(String applianceId, int fanSpeedLevel) {
135 if (fanSpeedLevel < 1 && fanSpeedLevel > 10) {
138 String commandJSON = "{ \"Fanspeed\": " + fanSpeedLevel + "}";
140 return sendCommand(commandJSON, applianceId);
141 } catch (ElectroluxAirException e) {
142 logger.warn("Work mode manual failed {}", e.getMessage());
148 public boolean setIonizer(String applianceId, String ionizerStatus) {
149 String commandJSON = "{ \"Ionizer\": " + ionizerStatus + "}";
151 return sendCommand(commandJSON, applianceId);
152 } catch (ElectroluxAirException e) {
153 logger.warn("Work mode manual failed {}", e.getMessage());
158 public boolean setUILight(String applianceId, String uiLightStatus) {
159 String commandJSON = "{ \"UILight\": " + uiLightStatus + "}";
161 return sendCommand(commandJSON, applianceId);
162 } catch (ElectroluxAirException e) {
163 logger.warn("Work mode manual failed {}", e.getMessage());
168 public boolean setSafetyLock(String applianceId, String safetyLockStatus) {
169 String commandJSON = "{ \"SafetyLock\": " + safetyLockStatus + "}";
171 return sendCommand(commandJSON, applianceId);
172 } catch (ElectroluxAirException e) {
173 logger.warn("Work mode manual failed {}", e.getMessage());
178 private Request createRequest(String uri, HttpMethod httpMethod) {
179 Request request = httpClient.newRequest(uri).method(httpMethod);
181 request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
182 request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
184 logger.debug("HTTP POST Request {}.", request.toString());
189 private void login() throws ElectroluxAirException {
191 String json = "{\"clientId\": \"" + CLIENT_ID + "\", \"clientSecret\": \"" + CLIENT_SECRET
192 + "\", \"grantType\": \"client_credentials\"}";
195 Request request = createRequest(TOKEN_URL, HttpMethod.POST);
196 request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
198 logger.debug("HTTP POST Request {}.", request.toString());
200 ContentResponse httpResponse = request.send();
201 if (httpResponse.getStatus() != HttpStatus.OK_200) {
202 throw new ElectroluxAirException("Failed to get token 1" + httpResponse.getContentAsString());
204 json = httpResponse.getContentAsString();
205 logger.trace("Token 1: {}", json);
206 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
207 String clientToken = jsonObject.get("accessToken").getAsString();
209 // Login using access token 1
210 json = "{ \"username\": \"" + configuration.username + "\", \"password\": \"" + configuration.password
212 request = createRequest(AUTHENTICATION_URL, HttpMethod.POST);
213 request.header(HttpHeader.AUTHORIZATION, "Bearer " + clientToken);
214 request.header("x-api-key", X_API_KEY);
216 request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
218 logger.debug("HTTP POST Request {}.", request.toString());
220 httpResponse = request.send();
221 if (httpResponse.getStatus() != HttpStatus.OK_200) {
222 throw new ElectroluxAirException("Failed to login " + httpResponse.getContentAsString());
224 json = httpResponse.getContentAsString();
225 logger.trace("Token 2: {}", json);
226 jsonObject = JsonParser.parseString(json).getAsJsonObject();
227 String idToken = jsonObject.get("idToken").getAsString();
228 String countryCode = jsonObject.get("countryCode").getAsString();
229 String credentials = "{\"clientId\": \"" + CLIENT_ID + "\", \"idToken\": \"" + idToken
230 + "\", \"grantType\": \"urn:ietf:params:oauth:grant-type:token-exchange\"}";
232 // Fetch access token 2
233 request = createRequest(TOKEN_URL, HttpMethod.POST);
234 request.header("Origin-Country-Code", countryCode);
235 request.content(new StringContentProvider(credentials), JSON_CONTENT_TYPE);
237 logger.debug("HTTP POST Request {}.", request.toString());
239 httpResponse = request.send();
240 if (httpResponse.getStatus() != HttpStatus.OK_200) {
241 throw new ElectroluxAirException("Failed to get token 1" + httpResponse.getContentAsString());
245 json = httpResponse.getContentAsString();
246 logger.trace("AccessToken: {}", json);
247 jsonObject = JsonParser.parseString(json).getAsJsonObject();
248 this.authToken = jsonObject.get("accessToken").getAsString();
249 int expiresIn = jsonObject.get("expiresIn").getAsInt();
250 this.tokenExpiry = Instant.now().plusSeconds(expiresIn);
251 } catch (InterruptedException | TimeoutException | ExecutionException e) {
252 throw new ElectroluxAirException(e);
256 private String getFromApi(String uri) throws ElectroluxAirException, InterruptedException {
258 for (int i = 0; i < MAX_RETRIES; i++) {
260 Request request = createRequest(uri, HttpMethod.GET);
261 request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
262 request.header("x-api-key", X_API_KEY);
264 ContentResponse response = request.send();
265 String content = response.getContentAsString();
266 logger.trace("API response: {}", content);
268 if (response.getStatus() != HttpStatus.OK_200) {
269 logger.debug("getFromApi failed, HTTP status: {}", response.getStatus());
274 } catch (TimeoutException e) {
275 logger.debug("TimeoutException error in get: {}", e.getMessage());
278 throw new ElectroluxAirException("Failed to fetch from API!");
279 } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
280 throw new ElectroluxAirException(e);
284 private String getAppliances() throws ElectroluxAirException {
286 return getFromApi(APPLIANCES_URL);
287 } catch (ElectroluxAirException | InterruptedException e) {
288 throw new ElectroluxAirException(e);
292 private String getAppliancesInfo(String applianceId) throws ElectroluxAirException {
294 return getFromApi(APPLIANCES_URL + "/" + applianceId + "/info");
295 } catch (ElectroluxAirException | InterruptedException e) {
296 throw new ElectroluxAirException(e);
300 private boolean sendCommand(String commandJSON, String applianceId) throws ElectroluxAirException {
302 for (int i = 0; i < MAX_RETRIES; i++) {
304 Request request = createRequest(APPLIANCES_URL + "/" + applianceId + "/command", HttpMethod.PUT);
305 request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
306 request.header("x-api-key", X_API_KEY);
307 request.content(new StringContentProvider(commandJSON), JSON_CONTENT_TYPE);
309 ContentResponse response = request.send();
310 String content = response.getContentAsString();
311 logger.trace("API response: {}", content);
313 if (response.getStatus() != HttpStatus.OK_200) {
314 logger.debug("sendCommand failed, HTTP status: {}", response.getStatus());
319 } catch (TimeoutException | InterruptedException e) {
320 logger.warn("TimeoutException error in get");
323 } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
324 throw new ElectroluxAirException(e);