2 * Copyright (c) 2010-2022 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.HashMap;
23 import java.util.List;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
29 import javax.ws.rs.core.HttpHeaders;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.util.StringContentProvider;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.http.HttpStatus;
39 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
40 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
41 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
42 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
43 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
44 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
45 import org.openhab.binding.homeconnect.internal.client.model.Data;
46 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
47 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
48 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
49 import org.openhab.binding.homeconnect.internal.client.model.Option;
50 import org.openhab.binding.homeconnect.internal.client.model.Program;
51 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
52 import org.openhab.core.auth.client.oauth2.OAuthClientService;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
56 import com.google.gson.JsonArray;
57 import com.google.gson.JsonObject;
60 * Client for Home Connect API.
62 * @author Jonas BrĂ¼stel - Initial contribution
63 * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
67 public class HomeConnectApiClient {
68 private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
69 private static final String BASE = "/api/homeappliances";
70 private static final String BASE_PATH = BASE + "/";
71 private static final int REQUEST_TIMEOUT_SEC = 30;
72 private static final int VALUE_TYPE_STRING = 0;
73 private static final int VALUE_TYPE_INT = 1;
74 private static final int VALUE_TYPE_BOOLEAN = 2;
75 private static final int COMMUNICATION_QUEUE_SIZE = 50;
77 private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
78 private final HttpClient client;
79 private final String apiUrl;
80 private final OAuthClientService oAuthClientService;
81 private final CircularQueue<ApiRequest> communicationQueue;
82 private final ApiBridgeConfiguration apiBridgeConfiguration;
84 public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
85 @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
86 this.client = httpClient;
87 this.oAuthClientService = oAuthClientService;
88 this.apiBridgeConfiguration = apiBridgeConfiguration;
90 apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
91 communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
92 if (apiRequestHistory != null) {
93 communicationQueue.addAll(apiRequestHistory);
98 * Get all home appliances
100 * @return list of {@link HomeAppliance}
101 * @throws CommunicationException API communication exception
102 * @throws AuthorizationException oAuth authorization exception
104 public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
105 Request request = createRequest(HttpMethod.GET, BASE);
107 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
108 checkResponseCode(HttpStatus.OK_200, request, response, null, null);
110 String responseBody = response.getContentAsString();
111 trackAndLogApiRequest(null, request, null, response, responseBody);
113 return mapToHomeAppliances(responseBody);
114 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
115 logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
116 trackAndLogApiRequest(null, request, null, null, null);
117 throw new CommunicationException(e);
122 * Get home appliance by id
124 * @param haId home appliance id
125 * @return {@link HomeAppliance}
126 * @throws CommunicationException API communication exception
127 * @throws AuthorizationException oAuth authorization exception
129 public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
130 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
132 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
133 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
135 String responseBody = response.getContentAsString();
136 trackAndLogApiRequest(haId, request, null, response, responseBody);
138 return mapToHomeAppliance(responseBody);
139 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
140 logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
141 trackAndLogApiRequest(haId, request, null, null, null);
142 throw new CommunicationException(e);
147 * Get ambient light state of device.
149 * @param haId home appliance id
150 * @return {@link Data}
151 * @throws CommunicationException API communication exception
152 * @throws AuthorizationException oAuth authorization exception
153 * @throws ApplianceOfflineException appliance is not connected to the cloud
155 public Data getAmbientLightState(String haId)
156 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
157 return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
161 * Set ambient light state of device.
163 * @param haId home appliance id
164 * @param enable enable or disable ambient light
165 * @throws CommunicationException API communication exception
166 * @throws AuthorizationException oAuth authorization exception
167 * @throws ApplianceOfflineException appliance is not connected to the cloud
169 public void setAmbientLightState(String haId, boolean enable)
170 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
171 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
175 * Get functional light state of device.
177 * @param haId home appliance id
178 * @return {@link Data}
179 * @throws CommunicationException API communication exception
180 * @throws AuthorizationException oAuth authorization exception
181 * @throws ApplianceOfflineException appliance is not connected to the cloud
183 public Data getFunctionalLightState(String haId)
184 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
185 return getSetting(haId, SETTING_LIGHTING);
189 * Set functional light state of device.
191 * @param haId home appliance id
192 * @param enable enable or disable functional light
193 * @throws CommunicationException API communication exception
194 * @throws AuthorizationException oAuth authorization exception
195 * @throws ApplianceOfflineException appliance is not connected to the cloud
197 public void setFunctionalLightState(String haId, boolean enable)
198 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
199 putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
203 * Get functional light brightness state of device.
205 * @param haId home appliance id
206 * @return {@link Data}
207 * @throws CommunicationException API communication exception
208 * @throws AuthorizationException oAuth authorization exception
209 * @throws ApplianceOfflineException appliance is not connected to the cloud
211 public Data getFunctionalLightBrightnessState(String haId)
212 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
213 return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
217 * Set functional light brightness of device.
219 * @param haId home appliance id
220 * @param value brightness value 10-100
221 * @throws CommunicationException API communication exception
222 * @throws AuthorizationException oAuth authorization exception
223 * @throws ApplianceOfflineException appliance is not connected to the cloud
225 public void setFunctionalLightBrightnessState(String haId, int value)
226 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
227 putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
231 * Get ambient light brightness state of device.
233 * @param haId home appliance id
234 * @return {@link Data}
235 * @throws CommunicationException API communication exception
236 * @throws AuthorizationException oAuth authorization exception
237 * @throws ApplianceOfflineException appliance is not connected to the cloud
239 public Data getAmbientLightBrightnessState(String haId)
240 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
241 return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
245 * Set ambient light brightness of device.
247 * @param haId home appliance id
248 * @param value brightness value 10-100
249 * @throws CommunicationException API communication exception
250 * @throws AuthorizationException oAuth authorization exception
251 * @throws ApplianceOfflineException appliance is not connected to the cloud
253 public void setAmbientLightBrightnessState(String haId, int value)
254 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
255 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
259 * Get ambient light color state of device.
261 * @param haId home appliance id
262 * @return {@link Data}
263 * @throws CommunicationException API communication exception
264 * @throws AuthorizationException oAuth authorization exception
265 * @throws ApplianceOfflineException appliance is not connected to the cloud
267 public Data getAmbientLightColorState(String haId)
268 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
269 return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
273 * Set ambient light color of device.
275 * @param haId home appliance id
276 * @param value color code
277 * @throws CommunicationException API communication exception
278 * @throws AuthorizationException oAuth authorization exception
279 * @throws ApplianceOfflineException appliance is not connected to the cloud
281 public void setAmbientLightColorState(String haId, String value)
282 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
283 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
287 * Get ambient light custom color state of device.
289 * @param haId home appliance id
290 * @return {@link Data}
291 * @throws CommunicationException API communication exception
292 * @throws AuthorizationException oAuth authorization exception
293 * @throws ApplianceOfflineException appliance is not connected to the cloud
295 public Data getAmbientLightCustomColorState(String haId)
296 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
297 return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
301 * Set ambient light color of device.
303 * @param haId home appliance id
304 * @param value color code
305 * @throws CommunicationException API communication exception
306 * @throws AuthorizationException oAuth authorization exception
307 * @throws ApplianceOfflineException appliance is not connected to the cloud
309 public void setAmbientLightCustomColorState(String haId, String value)
310 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
311 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
315 * Get power state of device.
317 * @param haId home appliance id
318 * @return {@link Data}
319 * @throws CommunicationException API communication exception
320 * @throws AuthorizationException oAuth authorization exception
321 * @throws ApplianceOfflineException appliance is not connected to the cloud
323 public Data getPowerState(String haId)
324 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
325 return getSetting(haId, SETTING_POWER_STATE);
329 * Set power state of device.
331 * @param haId home appliance id
332 * @param state target state
333 * @throws CommunicationException API communication exception
334 * @throws AuthorizationException oAuth authorization exception
335 * @throws ApplianceOfflineException appliance is not connected to the cloud
337 public void setPowerState(String haId, String state)
338 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
339 putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
343 * Get setpoint temperature of freezer
345 * @param haId home appliance id
346 * @return {@link Data}
347 * @throws CommunicationException API communication exception
348 * @throws AuthorizationException oAuth authorization exception
349 * @throws ApplianceOfflineException appliance is not connected to the cloud
351 public Data getFreezerSetpointTemperature(String haId)
352 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
353 return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
357 * Set setpoint temperature of freezer
359 * @param haId home appliance id
360 * @param state new temperature
361 * @param unit temperature unit
362 * @throws CommunicationException API communication exception
363 * @throws AuthorizationException oAuth authorization exception
364 * @throws ApplianceOfflineException appliance is not connected to the cloud
366 public void setFreezerSetpointTemperature(String haId, String state, String unit)
367 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
368 putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
372 * Get setpoint temperature of fridge
374 * @param haId home appliance id
375 * @return {@link Data} or null in case of communication error
376 * @throws CommunicationException API communication exception
377 * @throws AuthorizationException oAuth authorization exception
378 * @throws ApplianceOfflineException appliance is not connected to the cloud
380 public Data getFridgeSetpointTemperature(String haId)
381 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
382 return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
386 * Set setpoint temperature of fridge
388 * @param haId home appliance id
389 * @param state new temperature
390 * @param unit temperature unit
391 * @throws CommunicationException API communication exception
392 * @throws AuthorizationException oAuth authorization exception
393 * @throws ApplianceOfflineException appliance is not connected to the cloud
395 public void setFridgeSetpointTemperature(String haId, String state, String unit)
396 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
397 putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
401 * Get fridge super mode
403 * @param haId home appliance id
404 * @return {@link Data}
405 * @throws CommunicationException API communication exception
406 * @throws AuthorizationException oAuth authorization exception
407 * @throws ApplianceOfflineException appliance is not connected to the cloud
409 public Data getFridgeSuperMode(String haId)
410 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
411 return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
415 * Set fridge super mode
417 * @param haId home appliance id
418 * @param enable enable or disable fridge super mode
419 * @throws CommunicationException API communication exception
420 * @throws AuthorizationException oAuth authorization exception
421 * @throws ApplianceOfflineException appliance is not connected to the cloud
423 public void setFridgeSuperMode(String haId, boolean enable)
424 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
425 putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
429 * Get freezer super mode
431 * @param haId home appliance id
432 * @return {@link Data}
433 * @throws CommunicationException API communication exception
434 * @throws AuthorizationException oAuth authorization exception
435 * @throws ApplianceOfflineException appliance is not connected to the cloud
437 public Data getFreezerSuperMode(String haId)
438 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
439 return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
443 * Set freezer super mode
445 * @param haId home appliance id
446 * @param enable enable or disable freezer super mode
447 * @throws CommunicationException API communication exception
448 * @throws AuthorizationException oAuth authorization exception
449 * @throws ApplianceOfflineException appliance is not connected to the cloud
451 public void setFreezerSuperMode(String haId, boolean enable)
452 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
453 putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
457 * Get door state of device.
459 * @param haId home appliance id
460 * @return {@link Data}
461 * @throws CommunicationException API communication exception
462 * @throws AuthorizationException oAuth authorization exception
463 * @throws ApplianceOfflineException appliance is not connected to the cloud
465 public Data getDoorState(String haId)
466 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
467 return getStatus(haId, STATUS_DOOR_STATE);
471 * Get operation state of device.
473 * @param haId home appliance id
474 * @return {@link Data}
475 * @throws CommunicationException API communication exception
476 * @throws AuthorizationException oAuth authorization exception
477 * @throws ApplianceOfflineException appliance is not connected to the cloud
479 public Data getOperationState(String haId)
480 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
481 return getStatus(haId, STATUS_OPERATION_STATE);
485 * Get current cavity temperature of oven.
487 * @param haId home appliance id
488 * @return {@link Data}
489 * @throws CommunicationException API communication exception
490 * @throws AuthorizationException oAuth authorization exception
491 * @throws ApplianceOfflineException appliance is not connected to the cloud
493 public Data getCurrentCavityTemperature(String haId)
494 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
495 return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
499 * Is remote start allowed?
501 * @param haId haId home appliance id
502 * @return true or false
503 * @throws CommunicationException API communication exception
504 * @throws AuthorizationException oAuth authorization exception
505 * @throws ApplianceOfflineException appliance is not connected to the cloud
507 public boolean isRemoteControlStartAllowed(String haId)
508 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
509 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
510 return Boolean.parseBoolean(data.getValue());
514 * Is remote control allowed?
516 * @param haId haId home appliance id
517 * @return true or false
518 * @throws CommunicationException API communication exception
519 * @throws AuthorizationException oAuth authorization exception
520 * @throws ApplianceOfflineException appliance is not connected to the cloud
522 public boolean isRemoteControlActive(String haId)
523 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
524 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
525 return Boolean.parseBoolean(data.getValue());
529 * Is local control allowed?
531 * @param haId haId home appliance id
532 * @return true or false
533 * @throws CommunicationException API communication exception
534 * @throws AuthorizationException oAuth authorization exception
535 * @throws ApplianceOfflineException appliance is not connected to the cloud
537 public boolean isLocalControlActive(String haId)
538 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
539 Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
540 return Boolean.parseBoolean(data.getValue());
544 * Get active program of device.
546 * @param haId home appliance id
547 * @return {@link Program} or null if there is no active program
548 * @throws CommunicationException API communication exception
549 * @throws AuthorizationException oAuth authorization exception
550 * @throws ApplianceOfflineException appliance is not connected to the cloud
552 public @Nullable Program getActiveProgram(String haId)
553 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
554 return getProgram(haId, BASE_PATH + haId + "/programs/active");
558 * Get selected program of device.
560 * @param haId home appliance id
561 * @return {@link Program} or null if there is no selected program
562 * @throws CommunicationException API communication exception
563 * @throws AuthorizationException oAuth authorization exception
564 * @throws ApplianceOfflineException appliance is not connected to the cloud
566 public @Nullable Program getSelectedProgram(String haId)
567 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
568 return getProgram(haId, BASE_PATH + haId + "/programs/selected");
571 public void setSelectedProgram(String haId, String program)
572 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
573 putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
576 public void startProgram(String haId, String program)
577 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
578 putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
581 public void startSelectedProgram(String haId)
582 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
583 String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
584 if (selectedProgram != null) {
585 putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
589 public void startCustomProgram(String haId, String json)
590 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
591 putRaw(haId, BASE_PATH + haId + "/programs/active", json);
594 public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
595 boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
596 String programState = isProgramActive ? "active" : "selected";
598 putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
602 public void stopProgram(String haId)
603 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
604 sendDelete(haId, BASE_PATH + haId + "/programs/active");
607 public List<AvailableProgram> getPrograms(String haId)
608 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
609 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
612 public List<AvailableProgram> getAvailablePrograms(String haId)
613 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
614 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
618 * Get the available options of a program.
620 * @param haId home appliance id
621 * @param programKey program id
622 * @return list of {@link AvailableProgramOption} or null if the program is unsupported by the API
623 * @throws CommunicationException API communication exception
624 * @throws AuthorizationException oAuth authorization exception
625 * @throws ApplianceOfflineException appliance is not connected to the cloud
627 public @Nullable List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
628 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
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 return response.getStatus() == HttpStatus.OK_200 ? mapToAvailableProgramOption(responseBody, haId) : null;
645 } catch (InterruptedException | TimeoutException | ExecutionException e) {
646 logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
648 trackAndLogApiRequest(haId, request, null, null, null);
649 throw new CommunicationException(e);
654 * Get latest API requests.
656 * @return communication queue
658 public Collection<ApiRequest> getLatestApiRequests() {
659 return communicationQueue.getAll();
662 private Data getSetting(String haId, String setting)
663 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
664 return getData(haId, BASE_PATH + haId + "/settings/" + setting);
667 private void putSettings(String haId, Data data)
668 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
669 putSettings(haId, data, VALUE_TYPE_STRING);
672 private void putSettings(String haId, Data data, int valueType)
673 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
674 putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
677 private Data getStatus(String haId, String status)
678 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
679 return getData(haId, BASE_PATH + haId + "/status/" + status);
682 public @Nullable String getRaw(String haId, String path)
683 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
684 return getRaw(haId, path, false);
687 public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
688 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
689 Request request = createRequest(HttpMethod.GET, path);
691 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
692 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
694 String responseBody = response.getContentAsString();
695 trackAndLogApiRequest(haId, request, null, response, responseBody);
697 if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
700 } catch (InterruptedException | TimeoutException | ExecutionException e) {
701 logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
702 trackAndLogApiRequest(haId, request, null, null, null);
703 throw new CommunicationException(e);
708 public String putRaw(String haId, String path, String requestBodyPayload)
709 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
710 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
713 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
714 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
716 String responseBody = response.getContentAsString();
717 trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
719 } catch (InterruptedException | TimeoutException | ExecutionException e) {
720 logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
722 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
723 throw new CommunicationException(e);
727 private @Nullable Program getProgram(String haId, String path)
728 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
729 Request request = createRequest(HttpMethod.GET, path);
731 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
732 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
734 String responseBody = response.getContentAsString();
735 trackAndLogApiRequest(haId, request, null, response, responseBody);
737 if (response.getStatus() == HttpStatus.OK_200) {
738 return mapToProgram(responseBody);
740 } catch (InterruptedException | TimeoutException | ExecutionException e) {
741 logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
742 trackAndLogApiRequest(haId, request, null, null, null);
743 throw new CommunicationException(e);
748 private List<AvailableProgram> getAvailablePrograms(String haId, String path)
749 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
750 Request request = createRequest(HttpMethod.GET, path);
752 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
753 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
755 String responseBody = response.getContentAsString();
756 trackAndLogApiRequest(haId, request, null, response, responseBody);
758 return mapToAvailablePrograms(responseBody, haId);
759 } catch (InterruptedException | TimeoutException | ExecutionException e) {
760 logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
761 trackAndLogApiRequest(haId, request, null, null, null);
762 throw new CommunicationException(e);
766 private void sendDelete(String haId, String path)
767 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
768 Request request = createRequest(HttpMethod.DELETE, path);
770 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
771 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
773 trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
774 } catch (InterruptedException | TimeoutException | ExecutionException e) {
775 logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
776 trackAndLogApiRequest(haId, request, null, null, null);
777 throw new CommunicationException(e);
781 private Data getData(String haId, String path)
782 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
783 Request request = createRequest(HttpMethod.GET, path);
785 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
786 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
788 String responseBody = response.getContentAsString();
789 trackAndLogApiRequest(haId, request, null, response, responseBody);
791 return mapToState(responseBody);
792 } catch (InterruptedException | TimeoutException | ExecutionException e) {
793 logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
794 trackAndLogApiRequest(haId, request, null, null, null);
795 throw new CommunicationException(e);
799 private void putData(String haId, String path, Data data, int valueType)
800 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
801 JsonObject innerObject = new JsonObject();
802 innerObject.addProperty("key", data.getName());
804 if (data.getValue() != null) {
805 if (valueType == VALUE_TYPE_INT) {
806 innerObject.addProperty("value", data.getValueAsInt());
807 } else if (valueType == VALUE_TYPE_BOOLEAN) {
808 innerObject.addProperty("value", data.getValueAsBoolean());
810 innerObject.addProperty("value", data.getValue());
814 if (data.getUnit() != null) {
815 innerObject.addProperty("unit", data.getUnit());
818 JsonObject dataObject = new JsonObject();
819 dataObject.add("data", innerObject);
820 String requestBodyPayload = dataObject.toString();
822 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
825 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
826 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
828 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
829 } catch (InterruptedException | TimeoutException | ExecutionException e) {
830 logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
831 valueType, e.getMessage());
832 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
833 throw new CommunicationException(e);
837 private void putOption(String haId, String path, Option option, boolean asInt)
838 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
839 JsonObject innerObject = new JsonObject();
840 innerObject.addProperty("key", option.getKey());
842 if (option.getValue() != null) {
844 innerObject.addProperty("value", option.getValueAsInt());
846 innerObject.addProperty("value", option.getValue());
850 if (option.getUnit() != null) {
851 innerObject.addProperty("unit", option.getUnit());
854 JsonArray optionsArray = new JsonArray();
855 optionsArray.add(innerObject);
857 JsonObject optionsObject = new JsonObject();
858 optionsObject.add("options", optionsArray);
860 JsonObject dataObject = new JsonObject();
861 dataObject.add("data", optionsObject);
863 String requestBodyPayload = dataObject.toString();
865 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
868 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
869 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
871 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
872 } catch (InterruptedException | TimeoutException | ExecutionException e) {
873 logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
874 asInt, e.getMessage());
875 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
876 throw new CommunicationException(e);
880 private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
881 @Nullable String requestPayload)
882 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
883 checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload);
886 private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
887 @Nullable String haId, @Nullable String requestPayload)
888 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
889 if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
890 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
891 logger.debug("Current access token is invalid.");
892 String responseBody = response.getContentAsString();
893 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
894 throw new AuthorizationException("Token invalid!");
897 if (!desiredCodes.contains(response.getStatus())) {
898 int code = response.getStatus();
899 String message = response.getReason();
901 logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
902 String responseBody = response.getContentAsString();
903 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
905 responseBody = responseBody == null ? "" : responseBody;
906 if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
907 && responseBody.toLowerCase().contains("offline")) {
908 throw new ApplianceOfflineException(code, message, responseBody);
910 throw new CommunicationException(code, message, responseBody);
915 private Program mapToProgram(String json) {
916 ArrayList<Option> optionList = new ArrayList<>();
917 JsonObject responseObject = parseString(json).getAsJsonObject();
918 JsonObject data = responseObject.getAsJsonObject("data");
919 Program result = new Program(data.get("key").getAsString(), optionList);
920 JsonArray options = data.getAsJsonArray("options");
922 options.forEach(option -> {
923 JsonObject obj = (JsonObject) option;
926 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
928 String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
931 String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
933 optionList.add(new Option(key, value, unit));
939 private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
940 ArrayList<AvailableProgram> result = new ArrayList<>();
943 JsonObject responseObject = parseString(json).getAsJsonObject();
945 JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
946 programs.forEach(program -> {
947 JsonObject obj = (JsonObject) program;
949 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
950 JsonObject constraints = obj.getAsJsonObject("constraints");
951 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
953 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
956 if (key != null && execution != null) {
957 result.add(new AvailableProgram(key, available, execution));
960 } catch (Exception e) {
961 logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
967 private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
968 ArrayList<AvailableProgramOption> result = new ArrayList<>();
971 JsonObject responseObject = parseString(json).getAsJsonObject();
973 JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
974 options.forEach(option -> {
975 JsonObject obj = (JsonObject) option;
977 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
978 ArrayList<String> allowedValues = new ArrayList<>();
979 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
980 .forEach(value -> allowedValues.add(value.getAsString()));
983 result.add(new AvailableProgramOption(key, allowedValues));
986 } catch (Exception e) {
987 logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
993 private HomeAppliance mapToHomeAppliance(String json) {
994 JsonObject responseObject = parseString(json).getAsJsonObject();
996 JsonObject data = responseObject.getAsJsonObject("data");
998 return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
999 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
1000 data.get("type").getAsString(), data.get("enumber").getAsString());
1003 private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1004 final ArrayList<HomeAppliance> result = new ArrayList<>();
1005 JsonObject responseObject = parseString(json).getAsJsonObject();
1007 JsonObject data = responseObject.getAsJsonObject("data");
1008 JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1010 homeappliances.forEach(appliance -> {
1011 JsonObject obj = (JsonObject) appliance;
1013 result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1014 obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1015 obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1021 private Data mapToState(String json) {
1022 JsonObject responseObject = parseString(json).getAsJsonObject();
1024 JsonObject data = responseObject.getAsJsonObject("data");
1027 String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1029 return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1032 private Request createRequest(HttpMethod method, String path)
1033 throws AuthorizationException, CommunicationException {
1034 return client.newRequest(apiUrl + path)
1035 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1036 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1039 private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1040 @Nullable ContentResponse response, @Nullable String responseBody) {
1041 HomeConnectRequest homeConnectRequest = map(request, requestBody);
1043 HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1045 logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1046 trackApiRequest(homeConnectRequest, homeConnectResponse);
1049 private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1050 @Nullable HomeConnectResponse homeConnectResponse) {
1051 if (logger.isDebugEnabled()) {
1052 StringBuilder sb = new StringBuilder();
1055 sb.append("[").append(haId).append("] ");
1058 sb.append(homeConnectRequest.getMethod()).append(" ");
1059 if (homeConnectResponse != null) {
1060 sb.append(homeConnectResponse.getCode()).append(" ");
1062 sb.append(homeConnectRequest.getUrl()).append("\n");
1063 homeConnectRequest.getHeader()
1064 .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1066 if (homeConnectRequest.getBody() != null) {
1067 sb.append(homeConnectRequest.getBody()).append("\n");
1070 if (homeConnectResponse != null) {
1072 homeConnectResponse.getHeader()
1073 .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1075 if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1076 sb.append(homeConnectResponse.getBody()).append("\n");
1079 logger.debug("{}", sb.toString());
1083 private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1084 @Nullable HomeConnectResponse homeConnectResponse) {
1085 communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1088 private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1089 Map<String, String> headers = new HashMap<>();
1090 request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1092 return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1093 requestBody != null ? formatJsonBody(requestBody) : null);
1096 private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1097 Map<String, String> headers = new HashMap<>();
1098 response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1100 return new HomeConnectResponse(response.getStatus(), headers,
1101 responseBody != null ? formatJsonBody(responseBody) : null);