2 * Copyright (c) 2010-2021 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.homeconnect.internal.client;
15 import static java.util.Collections.singletonList;
16 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
17 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.*;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import javax.ws.rs.core.HttpHeaders;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
42 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
43 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
44 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
45 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
46 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
47 import org.openhab.binding.homeconnect.internal.client.model.Data;
48 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
49 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
50 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
51 import org.openhab.binding.homeconnect.internal.client.model.Option;
52 import org.openhab.binding.homeconnect.internal.client.model.Program;
53 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
54 import org.openhab.core.auth.client.oauth2.OAuthClientService;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
58 import com.google.gson.JsonArray;
59 import com.google.gson.JsonObject;
62 * Client for Home Connect API.
64 * @author Jonas BrĂ¼stel - Initial contribution
65 * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
69 public class HomeConnectApiClient {
70 private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
71 private static final String BASE = "/api/homeappliances";
72 private static final String BASE_PATH = BASE + "/";
73 private static final int REQUEST_TIMEOUT_SEC = 30;
74 private static final int VALUE_TYPE_STRING = 0;
75 private static final int VALUE_TYPE_INT = 1;
76 private static final int VALUE_TYPE_BOOLEAN = 2;
77 private static final int COMMUNICATION_QUEUE_SIZE = 50;
79 private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
80 private final HttpClient client;
81 private final String apiUrl;
82 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
83 private final OAuthClientService oAuthClientService;
84 private final CircularQueue<ApiRequest> communicationQueue;
85 private final ApiBridgeConfiguration apiBridgeConfiguration;
87 public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
88 @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
89 this.client = httpClient;
90 this.oAuthClientService = oAuthClientService;
91 this.apiBridgeConfiguration = apiBridgeConfiguration;
93 availableProgramOptionsCache = new ConcurrentHashMap<>();
94 apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
95 communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
96 if (apiRequestHistory != null) {
97 communicationQueue.addAll(apiRequestHistory);
102 * Get all home appliances
104 * @return list of {@link HomeAppliance}
105 * @throws CommunicationException API communication exception
106 * @throws AuthorizationException oAuth authorization exception
108 public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
109 Request request = createRequest(HttpMethod.GET, BASE);
111 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
112 checkResponseCode(HttpStatus.OK_200, request, response, null, null);
114 String responseBody = response.getContentAsString();
115 trackAndLogApiRequest(null, request, null, response, responseBody);
117 return mapToHomeAppliances(responseBody);
118 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
119 logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
120 trackAndLogApiRequest(null, request, null, null, null);
121 throw new CommunicationException(e);
126 * Get home appliance by id
128 * @param haId home appliance id
129 * @return {@link HomeAppliance}
130 * @throws CommunicationException API communication exception
131 * @throws AuthorizationException oAuth authorization exception
133 public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
134 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
136 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
137 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
139 String responseBody = response.getContentAsString();
140 trackAndLogApiRequest(haId, request, null, response, responseBody);
142 return mapToHomeAppliance(responseBody);
143 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
144 logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
145 trackAndLogApiRequest(haId, request, null, null, null);
146 throw new CommunicationException(e);
151 * Get ambient light state of device.
153 * @param haId home appliance id
154 * @return {@link Data}
155 * @throws CommunicationException API communication exception
156 * @throws AuthorizationException oAuth authorization exception
157 * @throws ApplianceOfflineException appliance is not connected to the cloud
159 public Data getAmbientLightState(String haId)
160 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
161 return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
165 * Set ambient light state of device.
167 * @param haId home appliance id
168 * @param enable enable or disable ambient light
169 * @throws CommunicationException API communication exception
170 * @throws AuthorizationException oAuth authorization exception
171 * @throws ApplianceOfflineException appliance is not connected to the cloud
173 public void setAmbientLightState(String haId, boolean enable)
174 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
175 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
179 * Get functional light state of device.
181 * @param haId home appliance id
182 * @return {@link Data}
183 * @throws CommunicationException API communication exception
184 * @throws AuthorizationException oAuth authorization exception
185 * @throws ApplianceOfflineException appliance is not connected to the cloud
187 public Data getFunctionalLightState(String haId)
188 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
189 return getSetting(haId, SETTING_LIGHTING);
193 * Set functional light state of device.
195 * @param haId home appliance id
196 * @param enable enable or disable functional light
197 * @throws CommunicationException API communication exception
198 * @throws AuthorizationException oAuth authorization exception
199 * @throws ApplianceOfflineException appliance is not connected to the cloud
201 public void setFunctionalLightState(String haId, boolean enable)
202 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
203 putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
207 * Get functional light brightness state of device.
209 * @param haId home appliance id
210 * @return {@link Data}
211 * @throws CommunicationException API communication exception
212 * @throws AuthorizationException oAuth authorization exception
213 * @throws ApplianceOfflineException appliance is not connected to the cloud
215 public Data getFunctionalLightBrightnessState(String haId)
216 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
217 return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
221 * Set functional light brightness of device.
223 * @param haId home appliance id
224 * @param value brightness value 10-100
225 * @throws CommunicationException API communication exception
226 * @throws AuthorizationException oAuth authorization exception
227 * @throws ApplianceOfflineException appliance is not connected to the cloud
229 public void setFunctionalLightBrightnessState(String haId, int value)
230 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
231 putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
235 * Get ambient light brightness state of device.
237 * @param haId home appliance id
238 * @return {@link Data}
239 * @throws CommunicationException API communication exception
240 * @throws AuthorizationException oAuth authorization exception
241 * @throws ApplianceOfflineException appliance is not connected to the cloud
243 public Data getAmbientLightBrightnessState(String haId)
244 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
245 return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
249 * Set ambient light brightness of device.
251 * @param haId home appliance id
252 * @param value brightness value 10-100
253 * @throws CommunicationException API communication exception
254 * @throws AuthorizationException oAuth authorization exception
255 * @throws ApplianceOfflineException appliance is not connected to the cloud
257 public void setAmbientLightBrightnessState(String haId, int value)
258 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
259 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
263 * Get ambient light color state of device.
265 * @param haId home appliance id
266 * @return {@link Data}
267 * @throws CommunicationException API communication exception
268 * @throws AuthorizationException oAuth authorization exception
269 * @throws ApplianceOfflineException appliance is not connected to the cloud
271 public Data getAmbientLightColorState(String haId)
272 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
273 return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
277 * Set ambient light color of device.
279 * @param haId home appliance id
280 * @param value color code
281 * @throws CommunicationException API communication exception
282 * @throws AuthorizationException oAuth authorization exception
283 * @throws ApplianceOfflineException appliance is not connected to the cloud
285 public void setAmbientLightColorState(String haId, String value)
286 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
287 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
291 * Get ambient light custom color state of device.
293 * @param haId home appliance id
294 * @return {@link Data}
295 * @throws CommunicationException API communication exception
296 * @throws AuthorizationException oAuth authorization exception
297 * @throws ApplianceOfflineException appliance is not connected to the cloud
299 public Data getAmbientLightCustomColorState(String haId)
300 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
301 return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
305 * Set ambient light color of device.
307 * @param haId home appliance id
308 * @param value color code
309 * @throws CommunicationException API communication exception
310 * @throws AuthorizationException oAuth authorization exception
311 * @throws ApplianceOfflineException appliance is not connected to the cloud
313 public void setAmbientLightCustomColorState(String haId, String value)
314 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
315 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
319 * Get power state of device.
321 * @param haId home appliance id
322 * @return {@link Data}
323 * @throws CommunicationException API communication exception
324 * @throws AuthorizationException oAuth authorization exception
325 * @throws ApplianceOfflineException appliance is not connected to the cloud
327 public Data getPowerState(String haId)
328 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
329 return getSetting(haId, SETTING_POWER_STATE);
333 * Set power state of device.
335 * @param haId home appliance id
336 * @param state target state
337 * @throws CommunicationException API communication exception
338 * @throws AuthorizationException oAuth authorization exception
339 * @throws ApplianceOfflineException appliance is not connected to the cloud
341 public void setPowerState(String haId, String state)
342 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
343 putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
347 * Get setpoint temperature of freezer
349 * @param haId home appliance id
350 * @return {@link Data}
351 * @throws CommunicationException API communication exception
352 * @throws AuthorizationException oAuth authorization exception
353 * @throws ApplianceOfflineException appliance is not connected to the cloud
355 public Data getFreezerSetpointTemperature(String haId)
356 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
357 return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
361 * Set setpoint temperature of freezer
363 * @param haId home appliance id
364 * @param state new temperature
365 * @param unit temperature unit
366 * @throws CommunicationException API communication exception
367 * @throws AuthorizationException oAuth authorization exception
368 * @throws ApplianceOfflineException appliance is not connected to the cloud
370 public void setFreezerSetpointTemperature(String haId, String state, String unit)
371 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
372 putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
376 * Get setpoint temperature of fridge
378 * @param haId home appliance id
379 * @return {@link Data} or null in case of communication error
380 * @throws CommunicationException API communication exception
381 * @throws AuthorizationException oAuth authorization exception
382 * @throws ApplianceOfflineException appliance is not connected to the cloud
384 public Data getFridgeSetpointTemperature(String haId)
385 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
386 return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
390 * Set setpoint temperature of fridge
392 * @param haId home appliance id
393 * @param state new temperature
394 * @param unit temperature unit
395 * @throws CommunicationException API communication exception
396 * @throws AuthorizationException oAuth authorization exception
397 * @throws ApplianceOfflineException appliance is not connected to the cloud
399 public void setFridgeSetpointTemperature(String haId, String state, String unit)
400 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
401 putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
405 * Get fridge super mode
407 * @param haId home appliance id
408 * @return {@link Data}
409 * @throws CommunicationException API communication exception
410 * @throws AuthorizationException oAuth authorization exception
411 * @throws ApplianceOfflineException appliance is not connected to the cloud
413 public Data getFridgeSuperMode(String haId)
414 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
415 return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
419 * Set fridge super mode
421 * @param haId home appliance id
422 * @param enable enable or disable fridge super mode
423 * @throws CommunicationException API communication exception
424 * @throws AuthorizationException oAuth authorization exception
425 * @throws ApplianceOfflineException appliance is not connected to the cloud
427 public void setFridgeSuperMode(String haId, boolean enable)
428 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
429 putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
433 * Get freezer super mode
435 * @param haId home appliance id
436 * @return {@link Data}
437 * @throws CommunicationException API communication exception
438 * @throws AuthorizationException oAuth authorization exception
439 * @throws ApplianceOfflineException appliance is not connected to the cloud
441 public Data getFreezerSuperMode(String haId)
442 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
443 return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
447 * Set freezer super mode
449 * @param haId home appliance id
450 * @param enable enable or disable freezer super mode
451 * @throws CommunicationException API communication exception
452 * @throws AuthorizationException oAuth authorization exception
453 * @throws ApplianceOfflineException appliance is not connected to the cloud
455 public void setFreezerSuperMode(String haId, boolean enable)
456 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
457 putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
461 * Get door state of device.
463 * @param haId home appliance id
464 * @return {@link Data}
465 * @throws CommunicationException API communication exception
466 * @throws AuthorizationException oAuth authorization exception
467 * @throws ApplianceOfflineException appliance is not connected to the cloud
469 public Data getDoorState(String haId)
470 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
471 return getStatus(haId, STATUS_DOOR_STATE);
475 * Get operation state of device.
477 * @param haId home appliance id
478 * @return {@link Data}
479 * @throws CommunicationException API communication exception
480 * @throws AuthorizationException oAuth authorization exception
481 * @throws ApplianceOfflineException appliance is not connected to the cloud
483 public Data getOperationState(String haId)
484 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
485 return getStatus(haId, STATUS_OPERATION_STATE);
489 * Get current cavity temperature of oven.
491 * @param haId home appliance id
492 * @return {@link Data}
493 * @throws CommunicationException API communication exception
494 * @throws AuthorizationException oAuth authorization exception
495 * @throws ApplianceOfflineException appliance is not connected to the cloud
497 public Data getCurrentCavityTemperature(String haId)
498 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
499 return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
503 * Is remote start allowed?
505 * @param haId haId home appliance id
506 * @return true or false
507 * @throws CommunicationException API communication exception
508 * @throws AuthorizationException oAuth authorization exception
509 * @throws ApplianceOfflineException appliance is not connected to the cloud
511 public boolean isRemoteControlStartAllowed(String haId)
512 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
513 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
514 return Boolean.parseBoolean(data.getValue());
518 * Is remote control allowed?
520 * @param haId haId home appliance id
521 * @return true or false
522 * @throws CommunicationException API communication exception
523 * @throws AuthorizationException oAuth authorization exception
524 * @throws ApplianceOfflineException appliance is not connected to the cloud
526 public boolean isRemoteControlActive(String haId)
527 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
528 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
529 return Boolean.parseBoolean(data.getValue());
533 * Is local control allowed?
535 * @param haId haId home appliance id
536 * @return true or false
537 * @throws CommunicationException API communication exception
538 * @throws AuthorizationException oAuth authorization exception
539 * @throws ApplianceOfflineException appliance is not connected to the cloud
541 public boolean isLocalControlActive(String haId)
542 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
543 Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
544 return Boolean.parseBoolean(data.getValue());
548 * Get active program of device.
550 * @param haId home appliance id
551 * @return {@link Data} or null if there is no active program
552 * @throws CommunicationException API communication exception
553 * @throws AuthorizationException oAuth authorization exception
554 * @throws ApplianceOfflineException appliance is not connected to the cloud
556 public @Nullable Program getActiveProgram(String haId)
557 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
558 return getProgram(haId, BASE_PATH + haId + "/programs/active");
562 * Get selected program of device.
564 * @param haId home appliance id
565 * @return {@link Data} or null if there is no selected program
566 * @throws CommunicationException API communication exception
567 * @throws AuthorizationException oAuth authorization exception
568 * @throws ApplianceOfflineException appliance is not connected to the cloud
570 public @Nullable Program getSelectedProgram(String haId)
571 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
572 return getProgram(haId, BASE_PATH + haId + "/programs/selected");
575 public void setSelectedProgram(String haId, String program)
576 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
577 putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
580 public void startProgram(String haId, String program)
581 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
582 putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
585 public void startSelectedProgram(String haId)
586 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
587 String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
588 if (selectedProgram != null) {
589 putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
593 public void startCustomProgram(String haId, String json)
594 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
595 putRaw(haId, BASE_PATH + haId + "/programs/active", json);
598 public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
599 boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
600 String programState = isProgramActive ? "active" : "selected";
602 putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
606 public void stopProgram(String haId)
607 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
608 sendDelete(haId, BASE_PATH + haId + "/programs/active");
611 public List<AvailableProgram> getPrograms(String haId)
612 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
613 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
616 public List<AvailableProgram> getAvailablePrograms(String haId)
617 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
618 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
621 public List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
622 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
623 if (availableProgramOptionsCache.containsKey(programKey)) {
624 logger.debug("Returning cached options for '{}'.", programKey);
625 List<AvailableProgramOption> availableProgramOptions = availableProgramOptionsCache.get(programKey);
626 return availableProgramOptions != null ? availableProgramOptions : Collections.emptyList();
629 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
631 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
632 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
634 String responseBody = response.getContentAsString();
635 trackAndLogApiRequest(haId, request, null, response, responseBody);
637 // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram"
638 if (response.getStatus() == HttpStatus.NOT_FOUND_404
639 && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) {
640 throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(),
641 responseBody == null ? "" : responseBody);
644 List<AvailableProgramOption> availableProgramOptions = response.getStatus() == HttpStatus.OK_200
645 ? mapToAvailableProgramOption(responseBody, haId)
647 availableProgramOptionsCache.put(programKey, availableProgramOptions);
648 return availableProgramOptions;
649 } catch (InterruptedException | TimeoutException | ExecutionException e) {
650 logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
652 trackAndLogApiRequest(haId, request, null, null, null);
653 throw new CommunicationException(e);
658 * Get latest API requests.
660 * @return communication queue
662 public Collection<ApiRequest> getLatestApiRequests() {
663 return communicationQueue.getAll();
666 private Data getSetting(String haId, String setting)
667 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
668 return getData(haId, BASE_PATH + haId + "/settings/" + setting);
671 private void putSettings(String haId, Data data)
672 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
673 putSettings(haId, data, VALUE_TYPE_STRING);
676 private void putSettings(String haId, Data data, int valueType)
677 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
678 putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
681 private Data getStatus(String haId, String status)
682 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
683 return getData(haId, BASE_PATH + haId + "/status/" + status);
686 public @Nullable String getRaw(String haId, String path)
687 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
688 return getRaw(haId, path, false);
691 public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
692 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
693 Request request = createRequest(HttpMethod.GET, path);
695 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
696 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
698 String responseBody = response.getContentAsString();
699 trackAndLogApiRequest(haId, request, null, response, responseBody);
701 if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
704 } catch (InterruptedException | TimeoutException | ExecutionException e) {
705 logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
706 trackAndLogApiRequest(haId, request, null, null, null);
707 throw new CommunicationException(e);
712 public String putRaw(String haId, String path, String requestBodyPayload)
713 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
714 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
717 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
718 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
720 String responseBody = response.getContentAsString();
721 trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
723 } catch (InterruptedException | TimeoutException | ExecutionException e) {
724 logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
726 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
727 throw new CommunicationException(e);
731 private @Nullable Program getProgram(String haId, String path)
732 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
733 Request request = createRequest(HttpMethod.GET, path);
735 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
736 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
738 String responseBody = response.getContentAsString();
739 trackAndLogApiRequest(haId, request, null, response, responseBody);
741 if (response.getStatus() == HttpStatus.OK_200) {
742 return mapToProgram(responseBody);
744 } catch (InterruptedException | TimeoutException | ExecutionException e) {
745 logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
746 trackAndLogApiRequest(haId, request, null, null, null);
747 throw new CommunicationException(e);
752 private List<AvailableProgram> getAvailablePrograms(String haId, String path)
753 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
754 Request request = createRequest(HttpMethod.GET, path);
756 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
757 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
759 String responseBody = response.getContentAsString();
760 trackAndLogApiRequest(haId, request, null, response, responseBody);
762 return mapToAvailablePrograms(responseBody, haId);
763 } catch (InterruptedException | TimeoutException | ExecutionException e) {
764 logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
765 trackAndLogApiRequest(haId, request, null, null, null);
766 throw new CommunicationException(e);
770 private void sendDelete(String haId, String path)
771 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
772 Request request = createRequest(HttpMethod.DELETE, path);
774 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
775 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
777 trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
778 } catch (InterruptedException | TimeoutException | ExecutionException e) {
779 logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
780 trackAndLogApiRequest(haId, request, null, null, null);
781 throw new CommunicationException(e);
785 private Data getData(String haId, String path)
786 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
787 Request request = createRequest(HttpMethod.GET, path);
789 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
790 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
792 String responseBody = response.getContentAsString();
793 trackAndLogApiRequest(haId, request, null, response, responseBody);
795 return mapToState(responseBody);
796 } catch (InterruptedException | TimeoutException | ExecutionException e) {
797 logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
798 trackAndLogApiRequest(haId, request, null, null, null);
799 throw new CommunicationException(e);
803 private void putData(String haId, String path, Data data, int valueType)
804 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
805 JsonObject innerObject = new JsonObject();
806 innerObject.addProperty("key", data.getName());
808 if (data.getValue() != null) {
809 if (valueType == VALUE_TYPE_INT) {
810 innerObject.addProperty("value", data.getValueAsInt());
811 } else if (valueType == VALUE_TYPE_BOOLEAN) {
812 innerObject.addProperty("value", data.getValueAsBoolean());
814 innerObject.addProperty("value", data.getValue());
818 if (data.getUnit() != null) {
819 innerObject.addProperty("unit", data.getUnit());
822 JsonObject dataObject = new JsonObject();
823 dataObject.add("data", innerObject);
824 String requestBodyPayload = dataObject.toString();
826 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
829 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
830 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
832 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
833 } catch (InterruptedException | TimeoutException | ExecutionException e) {
834 logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
835 valueType, e.getMessage());
836 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
837 throw new CommunicationException(e);
841 private void putOption(String haId, String path, Option option, boolean asInt)
842 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
843 JsonObject innerObject = new JsonObject();
844 innerObject.addProperty("key", option.getKey());
846 if (option.getValue() != null) {
848 innerObject.addProperty("value", option.getValueAsInt());
850 innerObject.addProperty("value", option.getValue());
854 if (option.getUnit() != null) {
855 innerObject.addProperty("unit", option.getUnit());
858 JsonArray optionsArray = new JsonArray();
859 optionsArray.add(innerObject);
861 JsonObject optionsObject = new JsonObject();
862 optionsObject.add("options", optionsArray);
864 JsonObject dataObject = new JsonObject();
865 dataObject.add("data", optionsObject);
867 String requestBodyPayload = dataObject.toString();
869 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
872 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
873 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
875 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
876 } catch (InterruptedException | TimeoutException | ExecutionException e) {
877 logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
878 asInt, e.getMessage());
879 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
880 throw new CommunicationException(e);
884 private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
885 @Nullable String requestPayload)
886 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
887 checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload);
890 private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
891 @Nullable String haId, @Nullable String requestPayload)
892 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
893 if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
894 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
895 logger.debug("Current access token is invalid.");
896 String responseBody = response.getContentAsString();
897 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
898 throw new AuthorizationException("Token invalid!");
901 if (!desiredCodes.contains(response.getStatus())) {
902 int code = response.getStatus();
903 String message = response.getReason();
905 logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
906 String responseBody = response.getContentAsString();
907 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
909 responseBody = responseBody == null ? "" : responseBody;
910 if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
911 && responseBody.toLowerCase().contains("offline")) {
912 throw new ApplianceOfflineException(code, message, responseBody);
914 throw new CommunicationException(code, message, responseBody);
919 private Program mapToProgram(String json) {
920 ArrayList<Option> optionList = new ArrayList<>();
921 JsonObject responseObject = parseString(json).getAsJsonObject();
922 JsonObject data = responseObject.getAsJsonObject("data");
923 Program result = new Program(data.get("key").getAsString(), optionList);
924 JsonArray options = data.getAsJsonArray("options");
926 options.forEach(option -> {
927 JsonObject obj = (JsonObject) option;
930 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
932 String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
935 String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
937 optionList.add(new Option(key, value, unit));
943 private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
944 ArrayList<AvailableProgram> result = new ArrayList<>();
947 JsonObject responseObject = parseString(json).getAsJsonObject();
949 JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
950 programs.forEach(program -> {
951 JsonObject obj = (JsonObject) program;
953 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
954 JsonObject constraints = obj.getAsJsonObject("constraints");
955 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
957 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
960 if (key != null && execution != null) {
961 result.add(new AvailableProgram(key, available, execution));
964 } catch (Exception e) {
965 logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
971 private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
972 ArrayList<AvailableProgramOption> result = new ArrayList<>();
975 JsonObject responseObject = parseString(json).getAsJsonObject();
977 JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
978 options.forEach(option -> {
979 JsonObject obj = (JsonObject) option;
981 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
982 ArrayList<String> allowedValues = new ArrayList<>();
983 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
984 .forEach(value -> allowedValues.add(value.getAsString()));
987 result.add(new AvailableProgramOption(key, allowedValues));
990 } catch (Exception e) {
991 logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
997 private HomeAppliance mapToHomeAppliance(String json) {
998 JsonObject responseObject = parseString(json).getAsJsonObject();
1000 JsonObject data = responseObject.getAsJsonObject("data");
1002 return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
1003 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
1004 data.get("type").getAsString(), data.get("enumber").getAsString());
1007 private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1008 final ArrayList<HomeAppliance> result = new ArrayList<>();
1009 JsonObject responseObject = parseString(json).getAsJsonObject();
1011 JsonObject data = responseObject.getAsJsonObject("data");
1012 JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1014 homeappliances.forEach(appliance -> {
1015 JsonObject obj = (JsonObject) appliance;
1017 result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1018 obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1019 obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1025 private Data mapToState(String json) {
1026 JsonObject responseObject = parseString(json).getAsJsonObject();
1028 JsonObject data = responseObject.getAsJsonObject("data");
1031 String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1033 return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1036 private Request createRequest(HttpMethod method, String path)
1037 throws AuthorizationException, CommunicationException {
1038 return client.newRequest(apiUrl + path)
1039 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1040 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1043 private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1044 @Nullable ContentResponse response, @Nullable String responseBody) {
1045 HomeConnectRequest homeConnectRequest = map(request, requestBody);
1047 HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1049 logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1050 trackApiRequest(homeConnectRequest, homeConnectResponse);
1053 private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1054 @Nullable HomeConnectResponse homeConnectResponse) {
1055 if (logger.isDebugEnabled()) {
1056 StringBuilder sb = new StringBuilder();
1059 sb.append("[").append(haId).append("] ");
1062 sb.append(homeConnectRequest.getMethod()).append(" ");
1063 if (homeConnectResponse != null) {
1064 sb.append(homeConnectResponse.getCode()).append(" ");
1066 sb.append(homeConnectRequest.getUrl()).append("\n");
1067 homeConnectRequest.getHeader()
1068 .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1070 if (homeConnectRequest.getBody() != null) {
1071 sb.append(homeConnectRequest.getBody()).append("\n");
1074 if (homeConnectResponse != null) {
1076 homeConnectResponse.getHeader()
1077 .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1079 if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1080 sb.append(homeConnectResponse.getBody()).append("\n");
1083 logger.debug("{}", sb.toString());
1087 private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1088 @Nullable HomeConnectResponse homeConnectResponse) {
1089 communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1092 private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1093 Map<String, String> headers = new HashMap<>();
1094 request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1096 return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1097 requestBody != null ? formatJsonBody(requestBody) : null);
1100 private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1101 Map<String, String> headers = new HashMap<>();
1102 response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1104 return new HomeConnectResponse(response.getStatus(), headers,
1105 responseBody != null ? formatJsonBody(responseBody) : null);