2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.homeconnect.internal.client;
15 import static java.util.Collections.singletonList;
16 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
17 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.*;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import javax.ws.rs.core.HttpHeaders;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
42 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
43 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
44 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
45 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
46 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
47 import org.openhab.binding.homeconnect.internal.client.model.Data;
48 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
49 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
50 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
51 import org.openhab.binding.homeconnect.internal.client.model.Option;
52 import org.openhab.binding.homeconnect.internal.client.model.Program;
53 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
54 import org.openhab.core.auth.client.oauth2.OAuthClientService;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
58 import com.google.gson.JsonArray;
59 import com.google.gson.JsonObject;
62 * Client for Home Connect API.
64 * @author Jonas BrĂ¼stel - Initial contribution
65 * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
69 public class HomeConnectApiClient {
70 private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
71 private static final String BASE = "/api/homeappliances";
72 private static final String BASE_PATH = BASE + "/";
73 private static final int REQUEST_TIMEOUT_SEC = 30;
74 private static final int VALUE_TYPE_STRING = 0;
75 private static final int VALUE_TYPE_INT = 1;
76 private static final int VALUE_TYPE_BOOLEAN = 2;
77 private static final int COMMUNICATION_QUEUE_SIZE = 50;
79 private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
80 private final HttpClient client;
81 private final String apiUrl;
82 private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
83 private final Map<String, List<AvailableProgram>> programsCache;
84 private final OAuthClientService oAuthClientService;
85 private final CircularQueue<ApiRequest> communicationQueue;
86 private final ApiBridgeConfiguration apiBridgeConfiguration;
88 public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
89 @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
90 this.client = httpClient;
91 this.oAuthClientService = oAuthClientService;
92 this.apiBridgeConfiguration = apiBridgeConfiguration;
94 availableProgramOptionsCache = new ConcurrentHashMap<>();
95 programsCache = new ConcurrentHashMap<>();
96 apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
97 communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
98 if (apiRequestHistory != null) {
99 communicationQueue.addAll(apiRequestHistory);
104 * Get all home appliances
106 * @return list of {@link HomeAppliance}
107 * @throws CommunicationException API communication exception
108 * @throws AuthorizationException oAuth authorization exception
110 public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
111 Request request = createRequest(HttpMethod.GET, BASE);
113 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
114 checkResponseCode(HttpStatus.OK_200, request, response, null, null);
116 String responseBody = response.getContentAsString();
117 trackAndLogApiRequest(null, request, null, response, responseBody);
119 return mapToHomeAppliances(responseBody);
120 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
121 logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
122 trackAndLogApiRequest(null, request, null, null, null);
123 throw new CommunicationException(e);
128 * Get home appliance by id
130 * @param haId home appliance id
131 * @return {@link HomeAppliance}
132 * @throws CommunicationException API communication exception
133 * @throws AuthorizationException oAuth authorization exception
135 public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
136 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
138 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
139 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
141 String responseBody = response.getContentAsString();
142 trackAndLogApiRequest(haId, request, null, response, responseBody);
144 return mapToHomeAppliance(responseBody);
145 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
146 logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
147 trackAndLogApiRequest(haId, request, null, null, null);
148 throw new CommunicationException(e);
153 * Get ambient light state of device.
155 * @param haId home appliance id
156 * @return {@link Data}
157 * @throws CommunicationException API communication exception
158 * @throws AuthorizationException oAuth authorization exception
159 * @throws ApplianceOfflineException appliance is not connected to the cloud
161 public Data getAmbientLightState(String haId)
162 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
163 return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
167 * Set ambient light state of device.
169 * @param haId home appliance id
170 * @param enable enable or disable ambient light
171 * @throws CommunicationException API communication exception
172 * @throws AuthorizationException oAuth authorization exception
173 * @throws ApplianceOfflineException appliance is not connected to the cloud
175 public void setAmbientLightState(String haId, boolean enable)
176 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
177 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
181 * Get functional light state of device.
183 * @param haId home appliance id
184 * @return {@link Data}
185 * @throws CommunicationException API communication exception
186 * @throws AuthorizationException oAuth authorization exception
187 * @throws ApplianceOfflineException appliance is not connected to the cloud
189 public Data getFunctionalLightState(String haId)
190 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
191 return getSetting(haId, SETTING_LIGHTING);
195 * Set functional light state of device.
197 * @param haId home appliance id
198 * @param enable enable or disable functional light
199 * @throws CommunicationException API communication exception
200 * @throws AuthorizationException oAuth authorization exception
201 * @throws ApplianceOfflineException appliance is not connected to the cloud
203 public void setFunctionalLightState(String haId, boolean enable)
204 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
205 putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
209 * Get functional light brightness state of device.
211 * @param haId home appliance id
212 * @return {@link Data}
213 * @throws CommunicationException API communication exception
214 * @throws AuthorizationException oAuth authorization exception
215 * @throws ApplianceOfflineException appliance is not connected to the cloud
217 public Data getFunctionalLightBrightnessState(String haId)
218 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
219 return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
223 * Set functional light brightness of device.
225 * @param haId home appliance id
226 * @param value brightness value 10-100
227 * @throws CommunicationException API communication exception
228 * @throws AuthorizationException oAuth authorization exception
229 * @throws ApplianceOfflineException appliance is not connected to the cloud
231 public void setFunctionalLightBrightnessState(String haId, int value)
232 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
233 putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
237 * Get ambient light brightness state of device.
239 * @param haId home appliance id
240 * @return {@link Data}
241 * @throws CommunicationException API communication exception
242 * @throws AuthorizationException oAuth authorization exception
243 * @throws ApplianceOfflineException appliance is not connected to the cloud
245 public Data getAmbientLightBrightnessState(String haId)
246 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
247 return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
251 * Set ambient light brightness of device.
253 * @param haId home appliance id
254 * @param value brightness value 10-100
255 * @throws CommunicationException API communication exception
256 * @throws AuthorizationException oAuth authorization exception
257 * @throws ApplianceOfflineException appliance is not connected to the cloud
259 public void setAmbientLightBrightnessState(String haId, int value)
260 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
261 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
265 * Get ambient light color state of device.
267 * @param haId home appliance id
268 * @return {@link Data}
269 * @throws CommunicationException API communication exception
270 * @throws AuthorizationException oAuth authorization exception
271 * @throws ApplianceOfflineException appliance is not connected to the cloud
273 public Data getAmbientLightColorState(String haId)
274 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
275 return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
279 * Set ambient light color of device.
281 * @param haId home appliance id
282 * @param value color code
283 * @throws CommunicationException API communication exception
284 * @throws AuthorizationException oAuth authorization exception
285 * @throws ApplianceOfflineException appliance is not connected to the cloud
287 public void setAmbientLightColorState(String haId, String value)
288 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
289 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
293 * Get ambient light custom color state of device.
295 * @param haId home appliance id
296 * @return {@link Data}
297 * @throws CommunicationException API communication exception
298 * @throws AuthorizationException oAuth authorization exception
299 * @throws ApplianceOfflineException appliance is not connected to the cloud
301 public Data getAmbientLightCustomColorState(String haId)
302 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
303 return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
307 * Set ambient light color of device.
309 * @param haId home appliance id
310 * @param value color code
311 * @throws CommunicationException API communication exception
312 * @throws AuthorizationException oAuth authorization exception
313 * @throws ApplianceOfflineException appliance is not connected to the cloud
315 public void setAmbientLightCustomColorState(String haId, String value)
316 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
317 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
321 * Get power state of device.
323 * @param haId home appliance id
324 * @return {@link Data}
325 * @throws CommunicationException API communication exception
326 * @throws AuthorizationException oAuth authorization exception
327 * @throws ApplianceOfflineException appliance is not connected to the cloud
329 public Data getPowerState(String haId)
330 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
331 return getSetting(haId, SETTING_POWER_STATE);
335 * Set power state of device.
337 * @param haId home appliance id
338 * @param state target state
339 * @throws CommunicationException API communication exception
340 * @throws AuthorizationException oAuth authorization exception
341 * @throws ApplianceOfflineException appliance is not connected to the cloud
343 public void setPowerState(String haId, String state)
344 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
345 putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
349 * Get setpoint temperature of freezer
351 * @param haId home appliance id
352 * @return {@link Data}
353 * @throws CommunicationException API communication exception
354 * @throws AuthorizationException oAuth authorization exception
355 * @throws ApplianceOfflineException appliance is not connected to the cloud
357 public Data getFreezerSetpointTemperature(String haId)
358 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
359 return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
363 * Set setpoint temperature of freezer
365 * @param haId home appliance id
366 * @param state new temperature
367 * @param unit temperature unit
368 * @throws CommunicationException API communication exception
369 * @throws AuthorizationException oAuth authorization exception
370 * @throws ApplianceOfflineException appliance is not connected to the cloud
372 public void setFreezerSetpointTemperature(String haId, String state, String unit)
373 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
374 putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
378 * Get setpoint temperature of fridge
380 * @param haId home appliance id
381 * @return {@link Data} or null in case of communication error
382 * @throws CommunicationException API communication exception
383 * @throws AuthorizationException oAuth authorization exception
384 * @throws ApplianceOfflineException appliance is not connected to the cloud
386 public Data getFridgeSetpointTemperature(String haId)
387 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
388 return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
392 * Set setpoint temperature of fridge
394 * @param haId home appliance id
395 * @param state new temperature
396 * @param unit temperature unit
397 * @throws CommunicationException API communication exception
398 * @throws AuthorizationException oAuth authorization exception
399 * @throws ApplianceOfflineException appliance is not connected to the cloud
401 public void setFridgeSetpointTemperature(String haId, String state, String unit)
402 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
403 putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
407 * Get fridge super mode
409 * @param haId home appliance id
410 * @return {@link Data}
411 * @throws CommunicationException API communication exception
412 * @throws AuthorizationException oAuth authorization exception
413 * @throws ApplianceOfflineException appliance is not connected to the cloud
415 public Data getFridgeSuperMode(String haId)
416 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
417 return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
421 * Set fridge super mode
423 * @param haId home appliance id
424 * @param enable enable or disable fridge super mode
425 * @throws CommunicationException API communication exception
426 * @throws AuthorizationException oAuth authorization exception
427 * @throws ApplianceOfflineException appliance is not connected to the cloud
429 public void setFridgeSuperMode(String haId, boolean enable)
430 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
431 putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
435 * Get freezer super mode
437 * @param haId home appliance id
438 * @return {@link Data}
439 * @throws CommunicationException API communication exception
440 * @throws AuthorizationException oAuth authorization exception
441 * @throws ApplianceOfflineException appliance is not connected to the cloud
443 public Data getFreezerSuperMode(String haId)
444 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
445 return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
449 * Set freezer super mode
451 * @param haId home appliance id
452 * @param enable enable or disable freezer super mode
453 * @throws CommunicationException API communication exception
454 * @throws AuthorizationException oAuth authorization exception
455 * @throws ApplianceOfflineException appliance is not connected to the cloud
457 public void setFreezerSuperMode(String haId, boolean enable)
458 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
459 putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
463 * Get door state of device.
465 * @param haId home appliance id
466 * @return {@link Data}
467 * @throws CommunicationException API communication exception
468 * @throws AuthorizationException oAuth authorization exception
469 * @throws ApplianceOfflineException appliance is not connected to the cloud
471 public Data getDoorState(String haId)
472 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
473 return getStatus(haId, STATUS_DOOR_STATE);
477 * Get operation state of device.
479 * @param haId home appliance id
480 * @return {@link Data}
481 * @throws CommunicationException API communication exception
482 * @throws AuthorizationException oAuth authorization exception
483 * @throws ApplianceOfflineException appliance is not connected to the cloud
485 public Data getOperationState(String haId)
486 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
487 return getStatus(haId, STATUS_OPERATION_STATE);
491 * Get current cavity temperature of oven.
493 * @param haId home appliance id
494 * @return {@link Data}
495 * @throws CommunicationException API communication exception
496 * @throws AuthorizationException oAuth authorization exception
497 * @throws ApplianceOfflineException appliance is not connected to the cloud
499 public Data getCurrentCavityTemperature(String haId)
500 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
501 return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
505 * Is remote start allowed?
507 * @param haId haId home appliance id
508 * @return true or false
509 * @throws CommunicationException API communication exception
510 * @throws AuthorizationException oAuth authorization exception
511 * @throws ApplianceOfflineException appliance is not connected to the cloud
513 public boolean isRemoteControlStartAllowed(String haId)
514 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
515 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
516 return Boolean.parseBoolean(data.getValue());
520 * Is remote control allowed?
522 * @param haId haId home appliance id
523 * @return true or false
524 * @throws CommunicationException API communication exception
525 * @throws AuthorizationException oAuth authorization exception
526 * @throws ApplianceOfflineException appliance is not connected to the cloud
528 public boolean isRemoteControlActive(String haId)
529 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
530 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
531 return Boolean.parseBoolean(data.getValue());
535 * Is local control allowed?
537 * @param haId haId home appliance id
538 * @return true or false
539 * @throws CommunicationException API communication exception
540 * @throws AuthorizationException oAuth authorization exception
541 * @throws ApplianceOfflineException appliance is not connected to the cloud
543 public boolean isLocalControlActive(String haId)
544 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
545 Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
546 return Boolean.parseBoolean(data.getValue());
550 * Get active program of device.
552 * @param haId home appliance id
553 * @return {@link Data} or null if there is no active program
554 * @throws CommunicationException API communication exception
555 * @throws AuthorizationException oAuth authorization exception
556 * @throws ApplianceOfflineException appliance is not connected to the cloud
558 public @Nullable Program getActiveProgram(String haId)
559 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
560 return getProgram(haId, BASE_PATH + haId + "/programs/active");
564 * Get selected program of device.
566 * @param haId home appliance id
567 * @return {@link Data} or null if there is no selected program
568 * @throws CommunicationException API communication exception
569 * @throws AuthorizationException oAuth authorization exception
570 * @throws ApplianceOfflineException appliance is not connected to the cloud
572 public @Nullable Program getSelectedProgram(String haId)
573 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
574 return getProgram(haId, BASE_PATH + haId + "/programs/selected");
577 public void setSelectedProgram(String haId, String program)
578 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
579 putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
582 public void startProgram(String haId, String program)
583 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
584 putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
587 public void startSelectedProgram(String haId)
588 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
589 String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
590 if (selectedProgram != null) {
591 putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
595 public void startCustomProgram(String haId, String json)
596 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
597 putRaw(haId, BASE_PATH + haId + "/programs/active", json);
600 public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
601 boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
602 String programState = isProgramActive ? "active" : "selected";
604 putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
608 public void stopProgram(String haId)
609 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
610 sendDelete(haId, BASE_PATH + haId + "/programs/active");
613 public List<AvailableProgram> getPrograms(String haId)
614 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
615 List<AvailableProgram> programs;
616 if (programsCache.containsKey(haId)) {
617 logger.debug("Returning cached programs for '{}'.", haId);
618 programs = programsCache.get(haId);
619 programs = programs != null ? programs : Collections.emptyList();
621 programs = getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
622 programsCache.put(haId, programs);
627 public List<AvailableProgram> getAvailablePrograms(String haId)
628 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
629 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
632 public List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
633 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
634 if (availableProgramOptionsCache.containsKey(programKey)) {
635 logger.debug("Returning cached options for '{}'.", programKey);
636 List<AvailableProgramOption> availableProgramOptions = availableProgramOptionsCache.get(programKey);
637 return availableProgramOptions != null ? availableProgramOptions : Collections.emptyList();
640 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
642 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
643 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
645 String responseBody = response.getContentAsString();
646 trackAndLogApiRequest(haId, request, null, response, responseBody);
648 // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram"
649 if (response.getStatus() == HttpStatus.NOT_FOUND_404
650 && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) {
651 throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(),
652 responseBody == null ? "" : responseBody);
655 List<AvailableProgramOption> availableProgramOptions = response.getStatus() == HttpStatus.OK_200
656 ? mapToAvailableProgramOption(responseBody, haId)
658 availableProgramOptionsCache.put(programKey, availableProgramOptions);
659 return availableProgramOptions;
660 } catch (InterruptedException | TimeoutException | ExecutionException e) {
661 logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
663 trackAndLogApiRequest(haId, request, null, null, null);
664 throw new CommunicationException(e);
669 * Get latest API requests.
671 * @return communication queue
673 public Collection<ApiRequest> getLatestApiRequests() {
674 return communicationQueue.getAll();
677 private Data getSetting(String haId, String setting)
678 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
679 return getData(haId, BASE_PATH + haId + "/settings/" + setting);
682 private void putSettings(String haId, Data data)
683 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
684 putSettings(haId, data, VALUE_TYPE_STRING);
687 private void putSettings(String haId, Data data, int valueType)
688 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
689 putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
692 private Data getStatus(String haId, String status)
693 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
694 return getData(haId, BASE_PATH + haId + "/status/" + status);
697 public @Nullable String getRaw(String haId, String path)
698 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
699 return getRaw(haId, path, false);
702 public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
703 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
704 Request request = createRequest(HttpMethod.GET, path);
706 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
707 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
709 String responseBody = response.getContentAsString();
710 trackAndLogApiRequest(haId, request, null, response, responseBody);
712 if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
715 } catch (InterruptedException | TimeoutException | ExecutionException e) {
716 logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
717 trackAndLogApiRequest(haId, request, null, null, null);
718 throw new CommunicationException(e);
723 public String putRaw(String haId, String path, String requestBodyPayload)
724 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
725 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
728 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
729 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
731 String responseBody = response.getContentAsString();
732 trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
734 } catch (InterruptedException | TimeoutException | ExecutionException e) {
735 logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
737 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
738 throw new CommunicationException(e);
742 private @Nullable Program getProgram(String haId, String path)
743 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
744 Request request = createRequest(HttpMethod.GET, path);
746 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
747 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
749 String responseBody = response.getContentAsString();
750 trackAndLogApiRequest(haId, request, null, response, responseBody);
752 if (response.getStatus() == HttpStatus.OK_200) {
753 return mapToProgram(responseBody);
755 } catch (InterruptedException | TimeoutException | ExecutionException e) {
756 logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
757 trackAndLogApiRequest(haId, request, null, null, null);
758 throw new CommunicationException(e);
763 private List<AvailableProgram> getAvailablePrograms(String haId, String path)
764 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
765 Request request = createRequest(HttpMethod.GET, path);
767 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
768 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
770 String responseBody = response.getContentAsString();
771 trackAndLogApiRequest(haId, request, null, response, responseBody);
773 return mapToAvailablePrograms(responseBody, haId);
774 } catch (InterruptedException | TimeoutException | ExecutionException e) {
775 logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
776 trackAndLogApiRequest(haId, request, null, null, null);
777 throw new CommunicationException(e);
781 private void sendDelete(String haId, String path)
782 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
783 Request request = createRequest(HttpMethod.DELETE, path);
785 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
786 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
788 trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
789 } catch (InterruptedException | TimeoutException | ExecutionException e) {
790 logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
791 trackAndLogApiRequest(haId, request, null, null, null);
792 throw new CommunicationException(e);
796 private Data getData(String haId, String path)
797 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
798 Request request = createRequest(HttpMethod.GET, path);
800 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
801 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
803 String responseBody = response.getContentAsString();
804 trackAndLogApiRequest(haId, request, null, response, responseBody);
806 return mapToState(responseBody);
807 } catch (InterruptedException | TimeoutException | ExecutionException e) {
808 logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
809 trackAndLogApiRequest(haId, request, null, null, null);
810 throw new CommunicationException(e);
814 private void putData(String haId, String path, Data data, int valueType)
815 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
816 JsonObject innerObject = new JsonObject();
817 innerObject.addProperty("key", data.getName());
819 if (data.getValue() != null) {
820 if (valueType == VALUE_TYPE_INT) {
821 innerObject.addProperty("value", data.getValueAsInt());
822 } else if (valueType == VALUE_TYPE_BOOLEAN) {
823 innerObject.addProperty("value", data.getValueAsBoolean());
825 innerObject.addProperty("value", data.getValue());
829 if (data.getUnit() != null) {
830 innerObject.addProperty("unit", data.getUnit());
833 JsonObject dataObject = new JsonObject();
834 dataObject.add("data", innerObject);
835 String requestBodyPayload = dataObject.toString();
837 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
840 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
841 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
843 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
844 } catch (InterruptedException | TimeoutException | ExecutionException e) {
845 logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
846 valueType, e.getMessage());
847 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
848 throw new CommunicationException(e);
852 private void putOption(String haId, String path, Option option, boolean asInt)
853 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
854 JsonObject innerObject = new JsonObject();
855 innerObject.addProperty("key", option.getKey());
857 if (option.getValue() != null) {
859 innerObject.addProperty("value", option.getValueAsInt());
861 innerObject.addProperty("value", option.getValue());
865 if (option.getUnit() != null) {
866 innerObject.addProperty("unit", option.getUnit());
869 JsonArray optionsArray = new JsonArray();
870 optionsArray.add(innerObject);
872 JsonObject optionsObject = new JsonObject();
873 optionsObject.add("options", optionsArray);
875 JsonObject dataObject = new JsonObject();
876 dataObject.add("data", optionsObject);
878 String requestBodyPayload = dataObject.toString();
880 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
883 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
884 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
886 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
887 } catch (InterruptedException | TimeoutException | ExecutionException e) {
888 logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
889 asInt, e.getMessage());
890 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
891 throw new CommunicationException(e);
895 private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
896 @Nullable String requestPayload)
897 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
898 checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload);
901 private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
902 @Nullable String haId, @Nullable String requestPayload)
903 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
904 if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
905 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
906 logger.debug("Current access token is invalid.");
907 String responseBody = response.getContentAsString();
908 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
909 throw new AuthorizationException("Token invalid!");
912 if (!desiredCodes.contains(response.getStatus())) {
913 int code = response.getStatus();
914 String message = response.getReason();
916 logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
917 String responseBody = response.getContentAsString();
918 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
920 responseBody = responseBody == null ? "" : responseBody;
921 if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
922 && responseBody.toLowerCase().contains("offline")) {
923 throw new ApplianceOfflineException(code, message, responseBody);
925 throw new CommunicationException(code, message, responseBody);
930 private Program mapToProgram(String json) {
931 ArrayList<Option> optionList = new ArrayList<>();
932 JsonObject responseObject = parseString(json).getAsJsonObject();
933 JsonObject data = responseObject.getAsJsonObject("data");
934 Program result = new Program(data.get("key").getAsString(), optionList);
935 JsonArray options = data.getAsJsonArray("options");
937 options.forEach(option -> {
938 JsonObject obj = (JsonObject) option;
941 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
943 String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
946 String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
948 optionList.add(new Option(key, value, unit));
954 private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
955 ArrayList<AvailableProgram> result = new ArrayList<>();
958 JsonObject responseObject = parseString(json).getAsJsonObject();
960 JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
961 programs.forEach(program -> {
962 JsonObject obj = (JsonObject) program;
964 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
965 JsonObject constraints = obj.getAsJsonObject("constraints");
966 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
968 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
971 if (key != null && execution != null) {
972 result.add(new AvailableProgram(key, available, execution));
975 } catch (Exception e) {
976 logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
982 private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
983 ArrayList<AvailableProgramOption> result = new ArrayList<>();
986 JsonObject responseObject = parseString(json).getAsJsonObject();
988 JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
989 options.forEach(option -> {
990 JsonObject obj = (JsonObject) option;
992 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
993 ArrayList<String> allowedValues = new ArrayList<>();
994 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
995 .forEach(value -> allowedValues.add(value.getAsString()));
998 result.add(new AvailableProgramOption(key, allowedValues));
1001 } catch (Exception e) {
1002 logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
1008 private HomeAppliance mapToHomeAppliance(String json) {
1009 JsonObject responseObject = parseString(json).getAsJsonObject();
1011 JsonObject data = responseObject.getAsJsonObject("data");
1013 return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
1014 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
1015 data.get("type").getAsString(), data.get("enumber").getAsString());
1018 private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1019 final ArrayList<HomeAppliance> result = new ArrayList<>();
1020 JsonObject responseObject = parseString(json).getAsJsonObject();
1022 JsonObject data = responseObject.getAsJsonObject("data");
1023 JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1025 homeappliances.forEach(appliance -> {
1026 JsonObject obj = (JsonObject) appliance;
1028 result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1029 obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1030 obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1036 private Data mapToState(String json) {
1037 JsonObject responseObject = parseString(json).getAsJsonObject();
1039 JsonObject data = responseObject.getAsJsonObject("data");
1042 String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1044 return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1047 private Request createRequest(HttpMethod method, String path)
1048 throws AuthorizationException, CommunicationException {
1049 return client.newRequest(apiUrl + path)
1050 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1051 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1054 private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1055 @Nullable ContentResponse response, @Nullable String responseBody) {
1056 HomeConnectRequest homeConnectRequest = map(request, requestBody);
1058 HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1060 logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1061 trackApiRequest(homeConnectRequest, homeConnectResponse);
1064 private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1065 @Nullable HomeConnectResponse homeConnectResponse) {
1066 if (logger.isDebugEnabled()) {
1067 StringBuilder sb = new StringBuilder();
1070 sb.append("[").append(haId).append("] ");
1073 sb.append(homeConnectRequest.getMethod()).append(" ");
1074 if (homeConnectResponse != null) {
1075 sb.append(homeConnectResponse.getCode()).append(" ");
1077 sb.append(homeConnectRequest.getUrl()).append("\n");
1078 homeConnectRequest.getHeader()
1079 .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1081 if (homeConnectRequest.getBody() != null) {
1082 sb.append(homeConnectRequest.getBody()).append("\n");
1085 if (homeConnectResponse != null) {
1087 homeConnectResponse.getHeader()
1088 .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1090 if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1091 sb.append(homeConnectResponse.getBody()).append("\n");
1094 logger.debug("{}", sb.toString());
1098 private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1099 @Nullable HomeConnectResponse homeConnectResponse) {
1100 communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1103 private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1104 Map<String, String> headers = new HashMap<>();
1105 request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1107 return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1108 requestBody != null ? formatJsonBody(requestBody) : null);
1111 private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1112 Map<String, String> headers = new HashMap<>();
1113 response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1115 return new HomeConnectResponse(response.getStatus(), headers,
1116 responseBody != null ? formatJsonBody(responseBody) : null);