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.TimeUnit;
19 import java.util.concurrent.TimeoutException;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jetty.client.HttpClient;
23 import org.eclipse.jetty.client.api.ContentResponse;
24 import org.eclipse.jetty.client.api.Request;
25 import org.eclipse.jetty.client.util.StringContentProvider;
26 import org.eclipse.jetty.http.HttpHeader;
27 import org.eclipse.jetty.http.HttpMethod;
28 import org.eclipse.jetty.http.HttpStatus;
29 import org.openhab.binding.electroluxair.internal.ElectroluxAirBridgeConfiguration;
30 import org.openhab.binding.electroluxair.internal.ElectroluxAirException;
31 import org.openhab.binding.electroluxair.internal.dto.ElectroluxPureA9DTO;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.Gson;
36 import com.google.gson.JsonObject;
37 import com.google.gson.JsonParser;
38 import com.google.gson.JsonSyntaxException;
41 * The {@link ElectroluxDeltaAPI} class defines the Elextrolux Delta API
43 * @author Jan Gustafsson - Initial contribution
46 public class ElectroluxDeltaAPI {
47 private static final String CLIENT_ID = "ElxOneApp";
48 private static final String CLIENT_SECRET = "8UKrsKD7jH9zvTV7rz5HeCLkit67Mmj68FvRVTlYygwJYy4dW6KF2cVLPKeWzUQUd6KJMtTifFf4NkDnjI7ZLdfnwcPtTSNtYvbP7OzEkmQD9IjhMOf5e1zeAQYtt2yN";
49 private static final String X_API_KEY = "2AMqwEV5MqVhTKrRCyYfVF8gmKrd2rAmp7cUsfky";
51 private static final String BASE_URL = "https://api.ocp.electrolux.one";
52 private static final String TOKEN_URL = BASE_URL + "/one-account-authorization/api/v1/token";
53 private static final String AUTHENTICATION_URL = BASE_URL + "/one-account-authentication/api/v1/authenticate";
54 private static final String API_URL = BASE_URL + "/appliance/api/v2";
55 private static final String APPLIANCES_URL = API_URL + "/appliances";
57 private static final String JSON_CONTENT_TYPE = "application/json";
58 private static final int MAX_RETRIES = 3;
59 private static final int REQUEST_TIMEOUT_MS = 10_000;
61 private final Logger logger = LoggerFactory.getLogger(ElectroluxDeltaAPI.class);
62 private final Gson gson;
63 private final HttpClient httpClient;
64 private final ElectroluxAirBridgeConfiguration configuration;
65 private String authToken = "";
66 private Instant tokenExpiry = Instant.MIN;
68 public ElectroluxDeltaAPI(ElectroluxAirBridgeConfiguration configuration, Gson gson, HttpClient httpClient) {
70 this.configuration = configuration;
71 this.httpClient = httpClient;
74 public boolean refresh(Map<String, ElectroluxPureA9DTO> electroluxAirThings) {
76 if (Instant.now().isAfter(this.tokenExpiry)) {
77 // Login again since token is expired
81 String json = getAppliances();
82 ElectroluxPureA9DTO[] dtos = gson.fromJson(json, ElectroluxPureA9DTO[].class);
84 for (ElectroluxPureA9DTO dto : dtos) {
85 String applianceId = dto.getApplianceId();
87 String jsonApplianceInfo = getAppliancesInfo(applianceId);
88 ElectroluxPureA9DTO.ApplianceInfo applianceInfo = gson.fromJson(jsonApplianceInfo,
89 ElectroluxPureA9DTO.ApplianceInfo.class);
90 if (applianceInfo != null) {
91 if ("AIR_PURIFIER".equals(applianceInfo.getDeviceType())) {
92 dto.setApplianceInfo(applianceInfo);
93 electroluxAirThings.put(dto.getProperties().getReported().getDeviceId(), dto);
99 } catch (JsonSyntaxException | ElectroluxAirException e) {
100 logger.warn("Failed to refresh! {}", e.getMessage());
105 public boolean workModePowerOff(String applianceId) {
106 String commandJSON = "{ \"WorkMode\": \"PowerOff\" }";
108 return sendCommand(commandJSON, applianceId);
109 } catch (ElectroluxAirException e) {
110 logger.warn("Work mode powerOff failed {}", e.getMessage());
115 public boolean workModeAuto(String applianceId) {
116 String commandJSON = "{ \"WorkMode\": \"Auto\" }";
118 return sendCommand(commandJSON, applianceId);
119 } catch (ElectroluxAirException e) {
120 logger.warn("Work mode auto failed {}", e.getMessage());
125 public boolean workModeManual(String applianceId) {
126 String commandJSON = "{ \"WorkMode\": \"Manual\" }";
128 return sendCommand(commandJSON, applianceId);
129 } catch (ElectroluxAirException e) {
130 logger.warn("Work mode manual failed {}", e.getMessage());
135 public boolean setFanSpeedLevel(String applianceId, int fanSpeedLevel) {
136 if (fanSpeedLevel < 1 && fanSpeedLevel > 10) {
139 String commandJSON = "{ \"Fanspeed\": " + fanSpeedLevel + "}";
141 return sendCommand(commandJSON, applianceId);
142 } catch (ElectroluxAirException e) {
143 logger.warn("Work mode manual failed {}", e.getMessage());
149 public boolean setIonizer(String applianceId, String ionizerStatus) {
150 String commandJSON = "{ \"Ionizer\": " + ionizerStatus + "}";
152 return sendCommand(commandJSON, applianceId);
153 } catch (ElectroluxAirException e) {
154 logger.warn("Work mode manual failed {}", e.getMessage());
159 public boolean setUILight(String applianceId, String uiLightStatus) {
160 String commandJSON = "{ \"UILight\": " + uiLightStatus + "}";
162 return sendCommand(commandJSON, applianceId);
163 } catch (ElectroluxAirException e) {
164 logger.warn("Work mode manual failed {}", e.getMessage());
169 public boolean setSafetyLock(String applianceId, String safetyLockStatus) {
170 String commandJSON = "{ \"SafetyLock\": " + safetyLockStatus + "}";
172 return sendCommand(commandJSON, applianceId);
173 } catch (ElectroluxAirException e) {
174 logger.warn("Work mode manual failed {}", e.getMessage());
179 private Request createRequest(String uri, HttpMethod httpMethod) {
180 Request request = httpClient.newRequest(uri).method(httpMethod);
181 request.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
182 request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
183 request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
185 logger.debug("HTTP POST Request {}.", request.toString());
190 private void login() throws ElectroluxAirException {
192 String json = "{\"clientId\": \"" + CLIENT_ID + "\", \"clientSecret\": \"" + CLIENT_SECRET
193 + "\", \"grantType\": \"client_credentials\"}";
196 Request request = createRequest(TOKEN_URL, HttpMethod.POST);
197 request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
199 logger.debug("HTTP POST Request {}.", request.toString());
201 ContentResponse httpResponse = request.send();
202 if (httpResponse.getStatus() != HttpStatus.OK_200) {
203 throw new ElectroluxAirException("Failed to get token 1" + httpResponse.getContentAsString());
205 json = httpResponse.getContentAsString();
206 logger.trace("Token 1: {}", json);
207 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
208 String clientToken = jsonObject.get("accessToken").getAsString();
210 // Login using access token 1
211 json = "{ \"username\": \"" + configuration.username + "\", \"password\": \"" + configuration.password
213 request = createRequest(AUTHENTICATION_URL, HttpMethod.POST);
214 request.header(HttpHeader.AUTHORIZATION, "Bearer " + clientToken);
215 request.header("x-api-key", X_API_KEY);
217 request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
219 logger.debug("HTTP POST Request {}.", request.toString());
221 httpResponse = request.send();
222 if (httpResponse.getStatus() != HttpStatus.OK_200) {
223 throw new ElectroluxAirException("Failed to login " + httpResponse.getContentAsString());
225 json = httpResponse.getContentAsString();
226 logger.trace("Token 2: {}", json);
227 jsonObject = JsonParser.parseString(json).getAsJsonObject();
228 String idToken = jsonObject.get("idToken").getAsString();
229 String countryCode = jsonObject.get("countryCode").getAsString();
230 String credentials = "{\"clientId\": \"" + CLIENT_ID + "\", \"idToken\": \"" + idToken
231 + "\", \"grantType\": \"urn:ietf:params:oauth:grant-type:token-exchange\"}";
233 // Fetch access token 2
234 request = createRequest(TOKEN_URL, HttpMethod.POST);
235 request.header("Origin-Country-Code", countryCode);
236 request.content(new StringContentProvider(credentials), JSON_CONTENT_TYPE);
238 logger.debug("HTTP POST Request {}.", request.toString());
240 httpResponse = request.send();
241 if (httpResponse.getStatus() != HttpStatus.OK_200) {
242 throw new ElectroluxAirException("Failed to get token 1" + httpResponse.getContentAsString());
246 json = httpResponse.getContentAsString();
247 logger.trace("AccessToken: {}", json);
248 jsonObject = JsonParser.parseString(json).getAsJsonObject();
249 this.authToken = jsonObject.get("accessToken").getAsString();
250 int expiresIn = jsonObject.get("expiresIn").getAsInt();
251 this.tokenExpiry = Instant.now().plusSeconds(expiresIn);
252 } catch (InterruptedException | TimeoutException | ExecutionException e) {
253 throw new ElectroluxAirException(e);
257 private String getFromApi(String uri) throws ElectroluxAirException, InterruptedException {
259 for (int i = 0; i < MAX_RETRIES; i++) {
261 Request request = createRequest(uri, HttpMethod.GET);
262 request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
263 request.header("x-api-key", X_API_KEY);
265 ContentResponse response = request.send();
266 String content = response.getContentAsString();
267 logger.trace("API response: {}", content);
269 if (response.getStatus() != HttpStatus.OK_200) {
270 logger.debug("getFromApi failed, HTTP status: {}", response.getStatus());
275 } catch (TimeoutException e) {
276 logger.debug("TimeoutException error in get: {}", e.getMessage());
279 throw new ElectroluxAirException("Failed to fetch from API!");
280 } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
281 throw new ElectroluxAirException(e);
285 private String getAppliances() throws ElectroluxAirException {
287 return getFromApi(APPLIANCES_URL);
288 } catch (ElectroluxAirException | InterruptedException e) {
289 throw new ElectroluxAirException(e);
293 private String getAppliancesInfo(String applianceId) throws ElectroluxAirException {
295 return getFromApi(APPLIANCES_URL + "/" + applianceId + "/info");
296 } catch (ElectroluxAirException | InterruptedException e) {
297 throw new ElectroluxAirException(e);
301 private boolean sendCommand(String commandJSON, String applianceId) throws ElectroluxAirException {
303 for (int i = 0; i < MAX_RETRIES; i++) {
305 Request request = createRequest(APPLIANCES_URL + "/" + applianceId + "/command", HttpMethod.PUT);
306 request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
307 request.header("x-api-key", X_API_KEY);
308 request.content(new StringContentProvider(commandJSON), JSON_CONTENT_TYPE);
310 ContentResponse response = request.send();
311 String content = response.getContentAsString();
312 logger.trace("API response: {}", content);
314 if (response.getStatus() != HttpStatus.OK_200) {
315 logger.debug("sendCommand failed, HTTP status: {}", response.getStatus());
320 } catch (TimeoutException | InterruptedException e) {
321 logger.warn("TimeoutException error in get");
324 } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
325 throw new ElectroluxAirException(e);