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.Arrays.asList;
16 import static java.util.Collections.singletonList;
17 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
18 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.formatJsonBody;
19 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.getAuthorizationHeader;
20 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.parseString;
21 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.sendRequest;
23 import java.time.ZonedDateTime;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
35 import javax.ws.rs.core.HttpHeaders;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.client.api.ContentResponse;
41 import org.eclipse.jetty.client.api.Request;
42 import org.eclipse.jetty.client.util.StringContentProvider;
43 import org.eclipse.jetty.http.HttpMethod;
44 import org.eclipse.jetty.http.HttpStatus;
45 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
46 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
47 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
48 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
49 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
50 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
51 import org.openhab.binding.homeconnect.internal.client.model.Data;
52 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
53 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
54 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
55 import org.openhab.binding.homeconnect.internal.client.model.Option;
56 import org.openhab.binding.homeconnect.internal.client.model.Program;
57 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
58 import org.openhab.core.auth.client.oauth2.OAuthClientService;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
62 import com.google.gson.JsonArray;
63 import com.google.gson.JsonObject;
66 * Client for Home Connect API.
68 * @author Jonas BrĂ¼stel - Initial contribution
69 * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
73 public class HomeConnectApiClient {
74 private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
75 private static final String BASE = "/api/homeappliances";
76 private static final String BASE_PATH = BASE + "/";
77 private static final int REQUEST_TIMEOUT_SEC = 30;
78 private static final int VALUE_TYPE_STRING = 0;
79 private static final int VALUE_TYPE_INT = 1;
80 private static final int VALUE_TYPE_BOOLEAN = 2;
81 private static final int COMMUNICATION_QUEUE_SIZE = 50;
83 private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
84 private final HttpClient client;
85 private final String apiUrl;
86 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
87 private final OAuthClientService oAuthClientService;
88 private final CircularQueue<ApiRequest> communicationQueue;
89 private final ApiBridgeConfiguration apiBridgeConfiguration;
91 public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
92 @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
93 this.client = httpClient;
94 this.oAuthClientService = oAuthClientService;
95 this.apiBridgeConfiguration = apiBridgeConfiguration;
97 availableProgramOptionsCache = new ConcurrentHashMap<>();
98 apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
99 communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
100 if (apiRequestHistory != null) {
101 communicationQueue.addAll(apiRequestHistory);
106 * Get all home appliances
108 * @return list of {@link HomeAppliance}
109 * @throws CommunicationException API communication exception
110 * @throws AuthorizationException oAuth authorization exception
112 public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
113 Request request = createRequest(HttpMethod.GET, BASE);
115 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
116 checkResponseCode(HttpStatus.OK_200, request, response, null, null);
118 String responseBody = response.getContentAsString();
119 trackAndLogApiRequest(null, request, null, response, responseBody);
121 return mapToHomeAppliances(responseBody);
122 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
123 logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
124 trackAndLogApiRequest(null, request, null, null, null);
125 throw new CommunicationException(e);
130 * Get home appliance by id
132 * @param haId home appliance id
133 * @return {@link HomeAppliance}
134 * @throws CommunicationException API communication exception
135 * @throws AuthorizationException oAuth authorization exception
137 public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
138 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
140 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
141 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
143 String responseBody = response.getContentAsString();
144 trackAndLogApiRequest(haId, request, null, response, responseBody);
146 return mapToHomeAppliance(responseBody);
147 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
148 logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
149 trackAndLogApiRequest(haId, request, null, null, null);
150 throw new CommunicationException(e);
155 * Get ambient light state of device.
157 * @param haId home appliance id
158 * @return {@link Data}
159 * @throws CommunicationException API communication exception
160 * @throws AuthorizationException oAuth authorization exception
161 * @throws ApplianceOfflineException appliance is not connected to the cloud
163 public Data getAmbientLightState(String haId)
164 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
165 return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
169 * Set ambient light state of device.
171 * @param haId home appliance id
172 * @param enable enable or disable ambient light
173 * @throws CommunicationException API communication exception
174 * @throws AuthorizationException oAuth authorization exception
175 * @throws ApplianceOfflineException appliance is not connected to the cloud
177 public void setAmbientLightState(String haId, boolean enable)
178 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
179 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
183 * Get functional light state of device.
185 * @param haId home appliance id
186 * @return {@link Data}
187 * @throws CommunicationException API communication exception
188 * @throws AuthorizationException oAuth authorization exception
189 * @throws ApplianceOfflineException appliance is not connected to the cloud
191 public Data getFunctionalLightState(String haId)
192 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
193 return getSetting(haId, SETTING_LIGHTING);
197 * Set functional light state of device.
199 * @param haId home appliance id
200 * @param enable enable or disable functional light
201 * @throws CommunicationException API communication exception
202 * @throws AuthorizationException oAuth authorization exception
203 * @throws ApplianceOfflineException appliance is not connected to the cloud
205 public void setFunctionalLightState(String haId, boolean enable)
206 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
207 putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
211 * Get functional light brightness state of device.
213 * @param haId home appliance id
214 * @return {@link Data}
215 * @throws CommunicationException API communication exception
216 * @throws AuthorizationException oAuth authorization exception
217 * @throws ApplianceOfflineException appliance is not connected to the cloud
219 public Data getFunctionalLightBrightnessState(String haId)
220 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
221 return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
225 * Set functional light brightness of device.
227 * @param haId home appliance id
228 * @param value brightness value 10-100
229 * @throws CommunicationException API communication exception
230 * @throws AuthorizationException oAuth authorization exception
231 * @throws ApplianceOfflineException appliance is not connected to the cloud
233 public void setFunctionalLightBrightnessState(String haId, int value)
234 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
235 putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
239 * Get ambient light brightness state of device.
241 * @param haId home appliance id
242 * @return {@link Data}
243 * @throws CommunicationException API communication exception
244 * @throws AuthorizationException oAuth authorization exception
245 * @throws ApplianceOfflineException appliance is not connected to the cloud
247 public Data getAmbientLightBrightnessState(String haId)
248 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
249 return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
253 * Set ambient light brightness of device.
255 * @param haId home appliance id
256 * @param value brightness value 10-100
257 * @throws CommunicationException API communication exception
258 * @throws AuthorizationException oAuth authorization exception
259 * @throws ApplianceOfflineException appliance is not connected to the cloud
261 public void setAmbientLightBrightnessState(String haId, int value)
262 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
263 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
267 * Get ambient light color state of device.
269 * @param haId home appliance id
270 * @return {@link Data}
271 * @throws CommunicationException API communication exception
272 * @throws AuthorizationException oAuth authorization exception
273 * @throws ApplianceOfflineException appliance is not connected to the cloud
275 public Data getAmbientLightColorState(String haId)
276 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
277 return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
281 * Set ambient light color of device.
283 * @param haId home appliance id
284 * @param value color code
285 * @throws CommunicationException API communication exception
286 * @throws AuthorizationException oAuth authorization exception
287 * @throws ApplianceOfflineException appliance is not connected to the cloud
289 public void setAmbientLightColorState(String haId, String value)
290 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
291 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
295 * Get ambient light custom color state of device.
297 * @param haId home appliance id
298 * @return {@link Data}
299 * @throws CommunicationException API communication exception
300 * @throws AuthorizationException oAuth authorization exception
301 * @throws ApplianceOfflineException appliance is not connected to the cloud
303 public Data getAmbientLightCustomColorState(String haId)
304 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
305 return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
309 * Set ambient light color of device.
311 * @param haId home appliance id
312 * @param value color code
313 * @throws CommunicationException API communication exception
314 * @throws AuthorizationException oAuth authorization exception
315 * @throws ApplianceOfflineException appliance is not connected to the cloud
317 public void setAmbientLightCustomColorState(String haId, String value)
318 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
319 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
323 * Get power state of device.
325 * @param haId home appliance id
326 * @return {@link Data}
327 * @throws CommunicationException API communication exception
328 * @throws AuthorizationException oAuth authorization exception
329 * @throws ApplianceOfflineException appliance is not connected to the cloud
331 public Data getPowerState(String haId)
332 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
333 return getSetting(haId, SETTING_POWER_STATE);
337 * Set power state of device.
339 * @param haId home appliance id
340 * @param state target state
341 * @throws CommunicationException API communication exception
342 * @throws AuthorizationException oAuth authorization exception
343 * @throws ApplianceOfflineException appliance is not connected to the cloud
345 public void setPowerState(String haId, String state)
346 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
347 putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
351 * Get setpoint temperature of freezer
353 * @param haId home appliance id
354 * @return {@link Data}
355 * @throws CommunicationException API communication exception
356 * @throws AuthorizationException oAuth authorization exception
357 * @throws ApplianceOfflineException appliance is not connected to the cloud
359 public Data getFreezerSetpointTemperature(String haId)
360 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
361 return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
365 * Set setpoint temperature of freezer
367 * @param haId home appliance id
368 * @param state new temperature
369 * @param unit temperature unit
370 * @throws CommunicationException API communication exception
371 * @throws AuthorizationException oAuth authorization exception
372 * @throws ApplianceOfflineException appliance is not connected to the cloud
374 public void setFreezerSetpointTemperature(String haId, String state, String unit)
375 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
376 putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
380 * Get setpoint temperature of fridge
382 * @param haId home appliance id
383 * @return {@link Data} or null in case of communication error
384 * @throws CommunicationException API communication exception
385 * @throws AuthorizationException oAuth authorization exception
386 * @throws ApplianceOfflineException appliance is not connected to the cloud
388 public Data getFridgeSetpointTemperature(String haId)
389 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
390 return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
394 * Set setpoint temperature of fridge
396 * @param haId home appliance id
397 * @param state new temperature
398 * @param unit temperature unit
399 * @throws CommunicationException API communication exception
400 * @throws AuthorizationException oAuth authorization exception
401 * @throws ApplianceOfflineException appliance is not connected to the cloud
403 public void setFridgeSetpointTemperature(String haId, String state, String unit)
404 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
405 putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
409 * Get fridge super mode
411 * @param haId home appliance id
412 * @return {@link Data}
413 * @throws CommunicationException API communication exception
414 * @throws AuthorizationException oAuth authorization exception
415 * @throws ApplianceOfflineException appliance is not connected to the cloud
417 public Data getFridgeSuperMode(String haId)
418 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
419 return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
423 * Set fridge super mode
425 * @param haId home appliance id
426 * @param enable enable or disable fridge super mode
427 * @throws CommunicationException API communication exception
428 * @throws AuthorizationException oAuth authorization exception
429 * @throws ApplianceOfflineException appliance is not connected to the cloud
431 public void setFridgeSuperMode(String haId, boolean enable)
432 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
433 putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
437 * Get freezer super mode
439 * @param haId home appliance id
440 * @return {@link Data}
441 * @throws CommunicationException API communication exception
442 * @throws AuthorizationException oAuth authorization exception
443 * @throws ApplianceOfflineException appliance is not connected to the cloud
445 public Data getFreezerSuperMode(String haId)
446 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
447 return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
451 * Set freezer super mode
453 * @param haId home appliance id
454 * @param enable enable or disable freezer super mode
455 * @throws CommunicationException API communication exception
456 * @throws AuthorizationException oAuth authorization exception
457 * @throws ApplianceOfflineException appliance is not connected to the cloud
459 public void setFreezerSuperMode(String haId, boolean enable)
460 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
461 putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
465 * Get door state of device.
467 * @param haId home appliance id
468 * @return {@link Data}
469 * @throws CommunicationException API communication exception
470 * @throws AuthorizationException oAuth authorization exception
471 * @throws ApplianceOfflineException appliance is not connected to the cloud
473 public Data getDoorState(String haId)
474 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
475 return getStatus(haId, STATUS_DOOR_STATE);
479 * Get operation state of device.
481 * @param haId home appliance id
482 * @return {@link Data}
483 * @throws CommunicationException API communication exception
484 * @throws AuthorizationException oAuth authorization exception
485 * @throws ApplianceOfflineException appliance is not connected to the cloud
487 public Data getOperationState(String haId)
488 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
489 return getStatus(haId, STATUS_OPERATION_STATE);
493 * Get current cavity temperature of oven.
495 * @param haId home appliance id
496 * @return {@link Data}
497 * @throws CommunicationException API communication exception
498 * @throws AuthorizationException oAuth authorization exception
499 * @throws ApplianceOfflineException appliance is not connected to the cloud
501 public Data getCurrentCavityTemperature(String haId)
502 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
503 return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
507 * Is remote start allowed?
509 * @param haId haId home appliance id
510 * @return true or false
511 * @throws CommunicationException API communication exception
512 * @throws AuthorizationException oAuth authorization exception
513 * @throws ApplianceOfflineException appliance is not connected to the cloud
515 public boolean isRemoteControlStartAllowed(String haId)
516 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
517 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
518 return Boolean.parseBoolean(data.getValue());
522 * Is remote control allowed?
524 * @param haId haId home appliance id
525 * @return true or false
526 * @throws CommunicationException API communication exception
527 * @throws AuthorizationException oAuth authorization exception
528 * @throws ApplianceOfflineException appliance is not connected to the cloud
530 public boolean isRemoteControlActive(String haId)
531 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
532 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
533 return Boolean.parseBoolean(data.getValue());
537 * Is local control allowed?
539 * @param haId haId home appliance id
540 * @return true or false
541 * @throws CommunicationException API communication exception
542 * @throws AuthorizationException oAuth authorization exception
543 * @throws ApplianceOfflineException appliance is not connected to the cloud
545 public boolean isLocalControlActive(String haId)
546 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
547 Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
548 return Boolean.parseBoolean(data.getValue());
552 * Get active program of device.
554 * @param haId home appliance id
555 * @return {@link Data} or null if there is no active program
556 * @throws CommunicationException API communication exception
557 * @throws AuthorizationException oAuth authorization exception
558 * @throws ApplianceOfflineException appliance is not connected to the cloud
560 public @Nullable Program getActiveProgram(String haId)
561 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
562 return getProgram(haId, BASE_PATH + haId + "/programs/active");
566 * Get selected program of device.
568 * @param haId home appliance id
569 * @return {@link Data} or null if there is no selected program
570 * @throws CommunicationException API communication exception
571 * @throws AuthorizationException oAuth authorization exception
572 * @throws ApplianceOfflineException appliance is not connected to the cloud
574 public @Nullable Program getSelectedProgram(String haId)
575 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
576 return getProgram(haId, BASE_PATH + haId + "/programs/selected");
579 public void setSelectedProgram(String haId, String program)
580 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
581 putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
584 public void startProgram(String haId, String program)
585 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
586 putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
589 public void startSelectedProgram(String haId)
590 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
591 String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
592 if (selectedProgram != null) {
593 putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
597 public void startCustomProgram(String haId, String json)
598 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
599 putRaw(haId, BASE_PATH + haId + "/programs/active", json);
602 public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
603 boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
604 String programState = isProgramActive ? "active" : "selected";
606 putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
610 public void stopProgram(String haId)
611 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
612 sendDelete(haId, BASE_PATH + haId + "/programs/active");
615 public List<AvailableProgram> getPrograms(String haId)
616 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
617 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
620 public List<AvailableProgram> getAvailablePrograms(String haId)
621 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
622 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
625 public List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
626 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
627 if (availableProgramOptionsCache.containsKey(programKey)) {
628 logger.debug("Returning cached options for '{}'.", programKey);
629 List<AvailableProgramOption> availableProgramOptions = availableProgramOptionsCache.get(programKey);
630 return availableProgramOptions != null ? availableProgramOptions : Collections.emptyList();
633 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
635 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
636 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
638 String responseBody = response.getContentAsString();
639 trackAndLogApiRequest(haId, request, null, response, responseBody);
641 List<AvailableProgramOption> availableProgramOptions = mapToAvailableProgramOption(responseBody, haId);
642 availableProgramOptionsCache.put(programKey, availableProgramOptions);
643 return availableProgramOptions;
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(asList(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(singletonList(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);