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());
103 public boolean workModePowerOff(String applianceId) {
104 String commandJSON = "{ \"WorkMode\": \"PowerOff\" }";
106 return sendCommand(commandJSON, applianceId);
107 } catch (ElectroluxAirException e) {
108 logger.warn("Work mode powerOff failed {}", e.getMessage());
113 public boolean workModeAuto(String applianceId) {
114 String commandJSON = "{ \"WorkMode\": \"Auto\" }";
116 return sendCommand(commandJSON, applianceId);
117 } catch (ElectroluxAirException e) {
118 logger.warn("Work mode auto failed {}", e.getMessage());
123 public boolean workModeManual(String applianceId) {
124 String commandJSON = "{ \"WorkMode\": \"Manual\" }";
126 return sendCommand(commandJSON, applianceId);
127 } catch (ElectroluxAirException e) {
128 logger.warn("Work mode manual failed {}", e.getMessage());
133 public boolean setFanSpeedLevel(String applianceId, int fanSpeedLevel) {
134 if (fanSpeedLevel < 1 && fanSpeedLevel > 10) {
137 String commandJSON = "{ \"Fanspeed\": " + fanSpeedLevel + "}";
139 return sendCommand(commandJSON, applianceId);
140 } catch (ElectroluxAirException e) {
141 logger.warn("Work mode manual failed {}", e.getMessage());
147 public boolean setIonizer(String applianceId, String ionizerStatus) {
148 String commandJSON = "{ \"Ionizer\": " + ionizerStatus + "}";
150 return sendCommand(commandJSON, applianceId);
151 } catch (ElectroluxAirException e) {
152 logger.warn("Work mode manual failed {}", e.getMessage());
157 public boolean setUILight(String applianceId, String uiLightStatus) {
158 String commandJSON = "{ \"UILight\": " + uiLightStatus + "}";
160 return sendCommand(commandJSON, applianceId);
161 } catch (ElectroluxAirException e) {
162 logger.warn("Work mode manual failed {}", e.getMessage());
167 public boolean setSafetyLock(String applianceId, String safetyLockStatus) {
168 String commandJSON = "{ \"SafetyLock\": " + safetyLockStatus + "}";
170 return sendCommand(commandJSON, applianceId);
171 } catch (ElectroluxAirException e) {
172 logger.warn("Work mode manual failed {}", e.getMessage());
177 private Request createRequest(String uri, HttpMethod httpMethod) {
178 Request request = httpClient.newRequest(uri).method(httpMethod);
180 request.header(HttpHeader.ACCEPT, JSON_CONTENT_TYPE);
181 request.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE);
183 logger.debug("HTTP POST Request {}.", request.toString());
188 private void login() throws ElectroluxAirException {
190 String json = "{\"clientId\": \"" + CLIENT_ID + "\", \"clientSecret\": \"" + CLIENT_SECRET
191 + "\", \"grantType\": \"client_credentials\"}";
194 Request request = createRequest(TOKEN_URL, HttpMethod.POST);
195 request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
197 logger.debug("HTTP POST Request {}.", request.toString());
199 ContentResponse httpResponse = request.send();
200 if (httpResponse.getStatus() != HttpStatus.OK_200) {
201 throw new ElectroluxAirException("Failed to get token 1" + httpResponse.getContentAsString());
203 json = httpResponse.getContentAsString();
204 logger.trace("Token 1: {}", json);
205 JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
206 String clientToken = jsonObject.get("accessToken").getAsString();
208 // Login using access token 1
209 json = "{ \"username\": \"" + configuration.username + "\", \"password\": \"" + configuration.password
211 request = createRequest(AUTHENTICATION_URL, HttpMethod.POST);
212 request.header(HttpHeader.AUTHORIZATION, "Bearer " + clientToken);
213 request.header("x-api-key", X_API_KEY);
215 request.content(new StringContentProvider(json), JSON_CONTENT_TYPE);
217 logger.debug("HTTP POST Request {}.", request.toString());
219 httpResponse = request.send();
220 if (httpResponse.getStatus() != HttpStatus.OK_200) {
221 throw new ElectroluxAirException("Failed to login " + httpResponse.getContentAsString());
223 json = httpResponse.getContentAsString();
224 logger.trace("Token 2: {}", json);
225 jsonObject = JsonParser.parseString(json).getAsJsonObject();
226 String idToken = jsonObject.get("idToken").getAsString();
227 String countryCode = jsonObject.get("countryCode").getAsString();
228 String credentials = "{\"clientId\": \"" + CLIENT_ID + "\", \"idToken\": \"" + idToken
229 + "\", \"grantType\": \"urn:ietf:params:oauth:grant-type:token-exchange\"}";
231 // Fetch access token 2
232 request = createRequest(TOKEN_URL, HttpMethod.POST);
233 request.header("Origin-Country-Code", countryCode);
234 request.content(new StringContentProvider(credentials), JSON_CONTENT_TYPE);
236 logger.debug("HTTP POST Request {}.", request.toString());
238 httpResponse = request.send();
239 if (httpResponse.getStatus() != HttpStatus.OK_200) {
240 throw new ElectroluxAirException("Failed to get token 1" + httpResponse.getContentAsString());
244 json = httpResponse.getContentAsString();
245 logger.trace("AccessToken: {}", json);
246 jsonObject = JsonParser.parseString(json).getAsJsonObject();
247 this.authToken = jsonObject.get("accessToken").getAsString();
248 int expiresIn = jsonObject.get("expiresIn").getAsInt();
249 this.tokenExpiry = Instant.now().plusSeconds(expiresIn);
250 } catch (InterruptedException | TimeoutException | ExecutionException e) {
251 throw new ElectroluxAirException(e);
255 private String getFromApi(String uri) throws ElectroluxAirException, InterruptedException {
257 for (int i = 0; i < MAX_RETRIES; i++) {
259 Request request = createRequest(uri, HttpMethod.GET);
260 request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
261 request.header("x-api-key", X_API_KEY);
263 ContentResponse response = request.send();
264 String content = response.getContentAsString();
265 logger.trace("API response: {}", content);
267 if (response.getStatus() != HttpStatus.OK_200) {
268 logger.debug("getFromApi failed, HTTP status: {}", response.getStatus());
273 } catch (TimeoutException e) {
274 logger.debug("TimeoutException error in get: {}", e.getMessage());
277 throw new ElectroluxAirException("Failed to fetch from API!");
278 } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
279 throw new ElectroluxAirException(e);
283 private String getAppliances() throws ElectroluxAirException {
285 return getFromApi(APPLIANCES_URL);
286 } catch (ElectroluxAirException | InterruptedException e) {
287 throw new ElectroluxAirException(e);
291 private String getAppliancesInfo(String applianceId) throws ElectroluxAirException {
293 return getFromApi(APPLIANCES_URL + "/" + applianceId + "/info");
294 } catch (ElectroluxAirException | InterruptedException e) {
295 throw new ElectroluxAirException(e);
299 private boolean sendCommand(String commandJSON, String applianceId) throws ElectroluxAirException {
301 for (int i = 0; i < MAX_RETRIES; i++) {
303 Request request = createRequest(APPLIANCES_URL + "/" + applianceId + "/command", HttpMethod.PUT);
304 request.header(HttpHeader.AUTHORIZATION, "Bearer " + authToken);
305 request.header("x-api-key", X_API_KEY);
306 request.content(new StringContentProvider(commandJSON), JSON_CONTENT_TYPE);
308 ContentResponse response = request.send();
309 String content = response.getContentAsString();
310 logger.trace("API response: {}", content);
312 if (response.getStatus() != HttpStatus.OK_200) {
313 logger.debug("sendCommand failed, HTTP status: {}", response.getStatus());
318 } catch (TimeoutException | InterruptedException e) {
319 logger.warn("TimeoutException error in get");
322 } catch (JsonSyntaxException | ElectroluxAirException | ExecutionException e) {
323 throw new ElectroluxAirException(e);