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.homeconnect.internal.client;
15 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
16 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.*;
18 import java.time.ZonedDateTime;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
28 import javax.ws.rs.core.HttpHeaders;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.util.StringContentProvider;
36 import org.eclipse.jetty.http.HttpMethod;
37 import org.eclipse.jetty.http.HttpStatus;
38 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
39 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
40 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
41 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
42 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
43 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
44 import org.openhab.binding.homeconnect.internal.client.model.Data;
45 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
46 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
47 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
48 import org.openhab.binding.homeconnect.internal.client.model.Option;
49 import org.openhab.binding.homeconnect.internal.client.model.Program;
50 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
51 import org.openhab.core.auth.client.oauth2.OAuthClientService;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
55 import com.google.gson.JsonArray;
56 import com.google.gson.JsonObject;
59 * Client for Home Connect API.
61 * @author Jonas BrĂ¼stel - Initial contribution
62 * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
66 public class HomeConnectApiClient {
67 private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
68 private static final String BASE = "/api/homeappliances";
69 private static final String BASE_PATH = BASE + "/";
70 private static final int REQUEST_TIMEOUT_SEC = 30;
71 private static final int VALUE_TYPE_STRING = 0;
72 private static final int VALUE_TYPE_INT = 1;
73 private static final int VALUE_TYPE_BOOLEAN = 2;
74 private static final int COMMUNICATION_QUEUE_SIZE = 50;
76 private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
77 private final HttpClient client;
78 private final String apiUrl;
79 private final OAuthClientService oAuthClientService;
80 private final CircularQueue<ApiRequest> communicationQueue;
81 private final ApiBridgeConfiguration apiBridgeConfiguration;
83 public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
84 @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
85 this.client = httpClient;
86 this.oAuthClientService = oAuthClientService;
87 this.apiBridgeConfiguration = apiBridgeConfiguration;
89 apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
90 communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
91 if (apiRequestHistory != null) {
92 communicationQueue.addAll(apiRequestHistory);
97 * Get all home appliances
99 * @return list of {@link HomeAppliance}
100 * @throws CommunicationException API communication exception
101 * @throws AuthorizationException oAuth authorization exception
103 public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
104 Request request = createRequest(HttpMethod.GET, BASE);
106 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
107 checkResponseCode(HttpStatus.OK_200, request, response, null, null);
109 String responseBody = response.getContentAsString();
110 trackAndLogApiRequest(null, request, null, response, responseBody);
112 return mapToHomeAppliances(responseBody);
113 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
114 logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
115 trackAndLogApiRequest(null, request, null, null, null);
116 throw new CommunicationException(e);
121 * Get home appliance by id
123 * @param haId home appliance id
124 * @return {@link HomeAppliance}
125 * @throws CommunicationException API communication exception
126 * @throws AuthorizationException oAuth authorization exception
128 public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
129 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
131 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
132 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
134 String responseBody = response.getContentAsString();
135 trackAndLogApiRequest(haId, request, null, response, responseBody);
137 return mapToHomeAppliance(responseBody);
138 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
139 logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
140 trackAndLogApiRequest(haId, request, null, null, null);
141 throw new CommunicationException(e);
146 * Get ambient light state of device.
148 * @param haId home appliance id
149 * @return {@link Data}
150 * @throws CommunicationException API communication exception
151 * @throws AuthorizationException oAuth authorization exception
152 * @throws ApplianceOfflineException appliance is not connected to the cloud
154 public Data getAmbientLightState(String haId)
155 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
156 return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
160 * Set ambient light state of device.
162 * @param haId home appliance id
163 * @param enable enable or disable ambient light
164 * @throws CommunicationException API communication exception
165 * @throws AuthorizationException oAuth authorization exception
166 * @throws ApplianceOfflineException appliance is not connected to the cloud
168 public void setAmbientLightState(String haId, boolean enable)
169 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
170 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
174 * Get functional light state of device.
176 * @param haId home appliance id
177 * @return {@link Data}
178 * @throws CommunicationException API communication exception
179 * @throws AuthorizationException oAuth authorization exception
180 * @throws ApplianceOfflineException appliance is not connected to the cloud
182 public Data getFunctionalLightState(String haId)
183 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
184 return getSetting(haId, SETTING_LIGHTING);
188 * Set functional light state of device.
190 * @param haId home appliance id
191 * @param enable enable or disable functional light
192 * @throws CommunicationException API communication exception
193 * @throws AuthorizationException oAuth authorization exception
194 * @throws ApplianceOfflineException appliance is not connected to the cloud
196 public void setFunctionalLightState(String haId, boolean enable)
197 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
198 putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
202 * Get functional light brightness state of device.
204 * @param haId home appliance id
205 * @return {@link Data}
206 * @throws CommunicationException API communication exception
207 * @throws AuthorizationException oAuth authorization exception
208 * @throws ApplianceOfflineException appliance is not connected to the cloud
210 public Data getFunctionalLightBrightnessState(String haId)
211 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
212 return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
216 * Set functional light brightness of device.
218 * @param haId home appliance id
219 * @param value brightness value 10-100
220 * @throws CommunicationException API communication exception
221 * @throws AuthorizationException oAuth authorization exception
222 * @throws ApplianceOfflineException appliance is not connected to the cloud
224 public void setFunctionalLightBrightnessState(String haId, int value)
225 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
226 putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
230 * Get ambient light brightness state of device.
232 * @param haId home appliance id
233 * @return {@link Data}
234 * @throws CommunicationException API communication exception
235 * @throws AuthorizationException oAuth authorization exception
236 * @throws ApplianceOfflineException appliance is not connected to the cloud
238 public Data getAmbientLightBrightnessState(String haId)
239 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
240 return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
244 * Set ambient light brightness of device.
246 * @param haId home appliance id
247 * @param value brightness value 10-100
248 * @throws CommunicationException API communication exception
249 * @throws AuthorizationException oAuth authorization exception
250 * @throws ApplianceOfflineException appliance is not connected to the cloud
252 public void setAmbientLightBrightnessState(String haId, int value)
253 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
254 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
258 * Get ambient light color state of device.
260 * @param haId home appliance id
261 * @return {@link Data}
262 * @throws CommunicationException API communication exception
263 * @throws AuthorizationException oAuth authorization exception
264 * @throws ApplianceOfflineException appliance is not connected to the cloud
266 public Data getAmbientLightColorState(String haId)
267 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
268 return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
272 * Set ambient light color of device.
274 * @param haId home appliance id
275 * @param value color code
276 * @throws CommunicationException API communication exception
277 * @throws AuthorizationException oAuth authorization exception
278 * @throws ApplianceOfflineException appliance is not connected to the cloud
280 public void setAmbientLightColorState(String haId, String value)
281 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
282 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
286 * Get ambient light custom color state of device.
288 * @param haId home appliance id
289 * @return {@link Data}
290 * @throws CommunicationException API communication exception
291 * @throws AuthorizationException oAuth authorization exception
292 * @throws ApplianceOfflineException appliance is not connected to the cloud
294 public Data getAmbientLightCustomColorState(String haId)
295 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
296 return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
300 * Set ambient light color of device.
302 * @param haId home appliance id
303 * @param value color code
304 * @throws CommunicationException API communication exception
305 * @throws AuthorizationException oAuth authorization exception
306 * @throws ApplianceOfflineException appliance is not connected to the cloud
308 public void setAmbientLightCustomColorState(String haId, String value)
309 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
310 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
314 * Get power state of device.
316 * @param haId home appliance id
317 * @return {@link Data}
318 * @throws CommunicationException API communication exception
319 * @throws AuthorizationException oAuth authorization exception
320 * @throws ApplianceOfflineException appliance is not connected to the cloud
322 public Data getPowerState(String haId)
323 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
324 return getSetting(haId, SETTING_POWER_STATE);
328 * Set power state of device.
330 * @param haId home appliance id
331 * @param state target state
332 * @throws CommunicationException API communication exception
333 * @throws AuthorizationException oAuth authorization exception
334 * @throws ApplianceOfflineException appliance is not connected to the cloud
336 public void setPowerState(String haId, String state)
337 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
338 putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
342 * Get setpoint temperature of freezer
344 * @param haId home appliance id
345 * @return {@link Data}
346 * @throws CommunicationException API communication exception
347 * @throws AuthorizationException oAuth authorization exception
348 * @throws ApplianceOfflineException appliance is not connected to the cloud
350 public Data getFreezerSetpointTemperature(String haId)
351 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
352 return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
356 * Set setpoint temperature of freezer
358 * @param haId home appliance id
359 * @param state new temperature
360 * @param unit temperature unit
361 * @throws CommunicationException API communication exception
362 * @throws AuthorizationException oAuth authorization exception
363 * @throws ApplianceOfflineException appliance is not connected to the cloud
365 public void setFreezerSetpointTemperature(String haId, String state, String unit)
366 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
367 putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
371 * Get setpoint temperature of fridge
373 * @param haId home appliance id
374 * @return {@link Data} or null in case of communication error
375 * @throws CommunicationException API communication exception
376 * @throws AuthorizationException oAuth authorization exception
377 * @throws ApplianceOfflineException appliance is not connected to the cloud
379 public Data getFridgeSetpointTemperature(String haId)
380 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
381 return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
385 * Set setpoint temperature of fridge
387 * @param haId home appliance id
388 * @param state new temperature
389 * @param unit temperature unit
390 * @throws CommunicationException API communication exception
391 * @throws AuthorizationException oAuth authorization exception
392 * @throws ApplianceOfflineException appliance is not connected to the cloud
394 public void setFridgeSetpointTemperature(String haId, String state, String unit)
395 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
396 putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
400 * Get fridge super mode
402 * @param haId home appliance id
403 * @return {@link Data}
404 * @throws CommunicationException API communication exception
405 * @throws AuthorizationException oAuth authorization exception
406 * @throws ApplianceOfflineException appliance is not connected to the cloud
408 public Data getFridgeSuperMode(String haId)
409 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
410 return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
414 * Set fridge super mode
416 * @param haId home appliance id
417 * @param enable enable or disable fridge super mode
418 * @throws CommunicationException API communication exception
419 * @throws AuthorizationException oAuth authorization exception
420 * @throws ApplianceOfflineException appliance is not connected to the cloud
422 public void setFridgeSuperMode(String haId, boolean enable)
423 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
424 putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
428 * Get freezer super mode
430 * @param haId home appliance id
431 * @return {@link Data}
432 * @throws CommunicationException API communication exception
433 * @throws AuthorizationException oAuth authorization exception
434 * @throws ApplianceOfflineException appliance is not connected to the cloud
436 public Data getFreezerSuperMode(String haId)
437 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
438 return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
442 * Set freezer super mode
444 * @param haId home appliance id
445 * @param enable enable or disable freezer super mode
446 * @throws CommunicationException API communication exception
447 * @throws AuthorizationException oAuth authorization exception
448 * @throws ApplianceOfflineException appliance is not connected to the cloud
450 public void setFreezerSuperMode(String haId, boolean enable)
451 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
452 putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
456 * Get door state of device.
458 * @param haId home appliance id
459 * @return {@link Data}
460 * @throws CommunicationException API communication exception
461 * @throws AuthorizationException oAuth authorization exception
462 * @throws ApplianceOfflineException appliance is not connected to the cloud
464 public Data getDoorState(String haId)
465 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
466 return getStatus(haId, STATUS_DOOR_STATE);
470 * Get operation state of device.
472 * @param haId home appliance id
473 * @return {@link Data}
474 * @throws CommunicationException API communication exception
475 * @throws AuthorizationException oAuth authorization exception
476 * @throws ApplianceOfflineException appliance is not connected to the cloud
478 public Data getOperationState(String haId)
479 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
480 return getStatus(haId, STATUS_OPERATION_STATE);
484 * Get current cavity temperature of oven.
486 * @param haId home appliance id
487 * @return {@link Data}
488 * @throws CommunicationException API communication exception
489 * @throws AuthorizationException oAuth authorization exception
490 * @throws ApplianceOfflineException appliance is not connected to the cloud
492 public Data getCurrentCavityTemperature(String haId)
493 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
494 return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
498 * Is remote start allowed?
500 * @param haId haId home appliance id
501 * @return true or false
502 * @throws CommunicationException API communication exception
503 * @throws AuthorizationException oAuth authorization exception
504 * @throws ApplianceOfflineException appliance is not connected to the cloud
506 public boolean isRemoteControlStartAllowed(String haId)
507 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
508 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
509 return Boolean.parseBoolean(data.getValue());
513 * Is remote control allowed?
515 * @param haId haId home appliance id
516 * @return true or false
517 * @throws CommunicationException API communication exception
518 * @throws AuthorizationException oAuth authorization exception
519 * @throws ApplianceOfflineException appliance is not connected to the cloud
521 public boolean isRemoteControlActive(String haId)
522 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
523 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
524 return Boolean.parseBoolean(data.getValue());
528 * Is local control allowed?
530 * @param haId haId home appliance id
531 * @return true or false
532 * @throws CommunicationException API communication exception
533 * @throws AuthorizationException oAuth authorization exception
534 * @throws ApplianceOfflineException appliance is not connected to the cloud
536 public boolean isLocalControlActive(String haId)
537 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
538 Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
539 return Boolean.parseBoolean(data.getValue());
543 * Get active program of device.
545 * @param haId home appliance id
546 * @return {@link Program} or null if there is no active program
547 * @throws CommunicationException API communication exception
548 * @throws AuthorizationException oAuth authorization exception
549 * @throws ApplianceOfflineException appliance is not connected to the cloud
551 public @Nullable Program getActiveProgram(String haId)
552 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
553 return getProgram(haId, BASE_PATH + haId + "/programs/active");
557 * Get selected program of device.
559 * @param haId home appliance id
560 * @return {@link Program} or null if there is no selected program
561 * @throws CommunicationException API communication exception
562 * @throws AuthorizationException oAuth authorization exception
563 * @throws ApplianceOfflineException appliance is not connected to the cloud
565 public @Nullable Program getSelectedProgram(String haId)
566 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
567 return getProgram(haId, BASE_PATH + haId + "/programs/selected");
570 public void setSelectedProgram(String haId, String program)
571 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
572 putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
575 public void startProgram(String haId, String program)
576 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
577 putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
580 public void startSelectedProgram(String haId)
581 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
582 String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
583 if (selectedProgram != null) {
584 putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
588 public void startCustomProgram(String haId, String json)
589 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
590 putRaw(haId, BASE_PATH + haId + "/programs/active", json);
593 public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
594 boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
595 String programState = isProgramActive ? "active" : "selected";
597 putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
601 public void stopProgram(String haId)
602 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
603 sendDelete(haId, BASE_PATH + haId + "/programs/active");
606 public List<AvailableProgram> getPrograms(String haId)
607 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
608 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
611 public List<AvailableProgram> getAvailablePrograms(String haId)
612 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
613 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
617 * Get the available options of a program.
619 * @param haId home appliance id
620 * @param programKey program id
621 * @return list of {@link AvailableProgramOption} or null if the program is unsupported by the API
622 * @throws CommunicationException API communication exception
623 * @throws AuthorizationException oAuth authorization exception
624 * @throws ApplianceOfflineException appliance is not connected to the cloud
626 public @Nullable List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
627 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
628 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
630 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
631 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
633 String responseBody = response.getContentAsString();
634 trackAndLogApiRequest(haId, request, null, response, responseBody);
636 // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram"
637 if (response.getStatus() == HttpStatus.NOT_FOUND_404
638 && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) {
639 throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(),
640 responseBody == null ? "" : responseBody);
643 return response.getStatus() == HttpStatus.OK_200 ? mapToAvailableProgramOption(responseBody, haId) : null;
644 } catch (InterruptedException | TimeoutException | ExecutionException e) {
645 logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
647 trackAndLogApiRequest(haId, request, null, null, null);
648 throw new CommunicationException(e);
653 * Get latest API requests.
655 * @return communication queue
657 public Collection<ApiRequest> getLatestApiRequests() {
658 return communicationQueue.getAll();
661 private Data getSetting(String haId, String setting)
662 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
663 return getData(haId, BASE_PATH + haId + "/settings/" + setting);
666 private void putSettings(String haId, Data data)
667 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
668 putSettings(haId, data, VALUE_TYPE_STRING);
671 private void putSettings(String haId, Data data, int valueType)
672 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
673 putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
676 private Data getStatus(String haId, String status)
677 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
678 return getData(haId, BASE_PATH + haId + "/status/" + status);
681 public @Nullable String getRaw(String haId, String path)
682 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
683 return getRaw(haId, path, false);
686 public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
687 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
688 Request request = createRequest(HttpMethod.GET, path);
690 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
691 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
693 String responseBody = response.getContentAsString();
694 trackAndLogApiRequest(haId, request, null, response, responseBody);
696 if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
699 } catch (InterruptedException | TimeoutException | ExecutionException e) {
700 logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
701 trackAndLogApiRequest(haId, request, null, null, null);
702 throw new CommunicationException(e);
707 public String putRaw(String haId, String path, String requestBodyPayload)
708 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
709 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
712 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
713 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
715 String responseBody = response.getContentAsString();
716 trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
718 } catch (InterruptedException | TimeoutException | ExecutionException e) {
719 logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
721 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
722 throw new CommunicationException(e);
726 private @Nullable Program getProgram(String haId, String path)
727 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
728 Request request = createRequest(HttpMethod.GET, path);
730 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
731 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
733 String responseBody = response.getContentAsString();
734 trackAndLogApiRequest(haId, request, null, response, responseBody);
736 if (response.getStatus() == HttpStatus.OK_200) {
737 return mapToProgram(responseBody);
739 } catch (InterruptedException | TimeoutException | ExecutionException e) {
740 logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
741 trackAndLogApiRequest(haId, request, null, null, null);
742 throw new CommunicationException(e);
747 private List<AvailableProgram> getAvailablePrograms(String haId, String path)
748 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
749 Request request = createRequest(HttpMethod.GET, path);
751 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
752 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
754 String responseBody = response.getContentAsString();
755 trackAndLogApiRequest(haId, request, null, response, responseBody);
757 return mapToAvailablePrograms(responseBody, haId);
758 } catch (InterruptedException | TimeoutException | ExecutionException e) {
759 logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
760 trackAndLogApiRequest(haId, request, null, null, null);
761 throw new CommunicationException(e);
765 private void sendDelete(String haId, String path)
766 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
767 Request request = createRequest(HttpMethod.DELETE, path);
769 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
770 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
772 trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
773 } catch (InterruptedException | TimeoutException | ExecutionException e) {
774 logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
775 trackAndLogApiRequest(haId, request, null, null, null);
776 throw new CommunicationException(e);
780 private Data getData(String haId, String path)
781 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
782 Request request = createRequest(HttpMethod.GET, path);
784 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
785 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
787 String responseBody = response.getContentAsString();
788 trackAndLogApiRequest(haId, request, null, response, responseBody);
790 return mapToState(responseBody);
791 } catch (InterruptedException | TimeoutException | ExecutionException e) {
792 logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
793 trackAndLogApiRequest(haId, request, null, null, null);
794 throw new CommunicationException(e);
798 private void putData(String haId, String path, Data data, int valueType)
799 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
800 JsonObject innerObject = new JsonObject();
801 innerObject.addProperty("key", data.getName());
803 if (data.getValue() != null) {
804 if (valueType == VALUE_TYPE_INT) {
805 innerObject.addProperty("value", data.getValueAsInt());
806 } else if (valueType == VALUE_TYPE_BOOLEAN) {
807 innerObject.addProperty("value", data.getValueAsBoolean());
809 innerObject.addProperty("value", data.getValue());
813 if (data.getUnit() != null) {
814 innerObject.addProperty("unit", data.getUnit());
817 JsonObject dataObject = new JsonObject();
818 dataObject.add("data", innerObject);
819 String requestBodyPayload = dataObject.toString();
821 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
824 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
825 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
827 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
828 } catch (InterruptedException | TimeoutException | ExecutionException e) {
829 logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
830 valueType, e.getMessage());
831 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
832 throw new CommunicationException(e);
836 private void putOption(String haId, String path, Option option, boolean asInt)
837 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
838 JsonObject innerObject = new JsonObject();
839 innerObject.addProperty("key", option.getKey());
841 if (option.getValue() != null) {
843 innerObject.addProperty("value", option.getValueAsInt());
845 innerObject.addProperty("value", option.getValue());
849 if (option.getUnit() != null) {
850 innerObject.addProperty("unit", option.getUnit());
853 JsonArray optionsArray = new JsonArray();
854 optionsArray.add(innerObject);
856 JsonObject optionsObject = new JsonObject();
857 optionsObject.add("options", optionsArray);
859 JsonObject dataObject = new JsonObject();
860 dataObject.add("data", optionsObject);
862 String requestBodyPayload = dataObject.toString();
864 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
867 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
868 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
870 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
871 } catch (InterruptedException | TimeoutException | ExecutionException e) {
872 logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
873 asInt, e.getMessage());
874 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
875 throw new CommunicationException(e);
879 private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
880 @Nullable String requestPayload)
881 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
882 checkResponseCode(List.of(desiredCode), request, response, haId, requestPayload);
885 private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
886 @Nullable String haId, @Nullable String requestPayload)
887 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
888 if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
889 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
890 logger.debug("Current access token is invalid.");
891 String responseBody = response.getContentAsString();
892 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
893 throw new AuthorizationException("Token invalid!");
896 if (!desiredCodes.contains(response.getStatus())) {
897 int code = response.getStatus();
898 String message = response.getReason();
900 logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
901 String responseBody = response.getContentAsString();
902 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
904 responseBody = responseBody == null ? "" : responseBody;
905 if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
906 && responseBody.toLowerCase().contains("offline")) {
907 throw new ApplianceOfflineException(code, message, responseBody);
909 throw new CommunicationException(code, message, responseBody);
914 private Program mapToProgram(String json) {
915 ArrayList<Option> optionList = new ArrayList<>();
916 JsonObject responseObject = parseString(json).getAsJsonObject();
917 JsonObject data = responseObject.getAsJsonObject("data");
918 Program result = new Program(data.get("key").getAsString(), optionList);
919 JsonArray options = data.getAsJsonArray("options");
921 options.forEach(option -> {
922 JsonObject obj = (JsonObject) option;
925 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
927 String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
930 String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
932 optionList.add(new Option(key, value, unit));
938 private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
939 ArrayList<AvailableProgram> result = new ArrayList<>();
942 JsonObject responseObject = parseString(json).getAsJsonObject();
944 JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
945 programs.forEach(program -> {
946 JsonObject obj = (JsonObject) program;
948 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
949 JsonObject constraints = obj.getAsJsonObject("constraints");
950 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
952 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
955 if (key != null && execution != null) {
956 result.add(new AvailableProgram(key, available, execution));
959 } catch (Exception e) {
960 logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
966 private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
967 ArrayList<AvailableProgramOption> result = new ArrayList<>();
970 JsonObject responseObject = parseString(json).getAsJsonObject();
972 JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
973 options.forEach(option -> {
974 JsonObject obj = (JsonObject) option;
976 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
977 ArrayList<String> allowedValues = new ArrayList<>();
978 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
979 .forEach(value -> allowedValues.add(value.getAsString()));
982 result.add(new AvailableProgramOption(key, allowedValues));
985 } catch (Exception e) {
986 logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
992 private HomeAppliance mapToHomeAppliance(String json) {
993 JsonObject responseObject = parseString(json).getAsJsonObject();
995 JsonObject data = responseObject.getAsJsonObject("data");
997 return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
998 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
999 data.get("type").getAsString(), data.get("enumber").getAsString());
1002 private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1003 final ArrayList<HomeAppliance> result = new ArrayList<>();
1004 JsonObject responseObject = parseString(json).getAsJsonObject();
1006 JsonObject data = responseObject.getAsJsonObject("data");
1007 JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1009 homeappliances.forEach(appliance -> {
1010 JsonObject obj = (JsonObject) appliance;
1012 result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1013 obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1014 obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1020 private Data mapToState(String json) {
1021 JsonObject responseObject = parseString(json).getAsJsonObject();
1023 JsonObject data = responseObject.getAsJsonObject("data");
1026 String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1028 return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1031 private Request createRequest(HttpMethod method, String path)
1032 throws AuthorizationException, CommunicationException {
1033 return client.newRequest(apiUrl + path)
1034 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1035 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1038 private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1039 @Nullable ContentResponse response, @Nullable String responseBody) {
1040 HomeConnectRequest homeConnectRequest = map(request, requestBody);
1042 HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1044 logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1045 trackApiRequest(homeConnectRequest, homeConnectResponse);
1048 private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1049 @Nullable HomeConnectResponse homeConnectResponse) {
1050 if (logger.isDebugEnabled()) {
1051 StringBuilder sb = new StringBuilder();
1054 sb.append("[").append(haId).append("] ");
1057 sb.append(homeConnectRequest.getMethod()).append(" ");
1058 if (homeConnectResponse != null) {
1059 sb.append(homeConnectResponse.getCode()).append(" ");
1061 sb.append(homeConnectRequest.getUrl()).append("\n");
1062 homeConnectRequest.getHeader()
1063 .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1065 if (homeConnectRequest.getBody() != null) {
1066 sb.append(homeConnectRequest.getBody()).append("\n");
1069 if (homeConnectResponse != null) {
1071 homeConnectResponse.getHeader()
1072 .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1074 if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1075 sb.append(homeConnectResponse.getBody()).append("\n");
1078 logger.debug("{}", sb.toString());
1082 private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1083 @Nullable HomeConnectResponse homeConnectResponse) {
1084 communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1087 private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1088 Map<String, String> headers = new HashMap<>();
1089 request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1091 return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1092 requestBody != null ? formatJsonBody(requestBody) : null);
1095 private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1096 Map<String, String> headers = new HashMap<>();
1097 response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1099 return new HomeConnectResponse(response.getStatus(), headers,
1100 responseBody != null ? formatJsonBody(responseBody) : null);