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<AvailableProgram>> programsCache;
83 private final OAuthClientService oAuthClientService;
84 private final CircularQueue<ApiRequest> communicationQueue;
85 private final ApiBridgeConfiguration apiBridgeConfiguration;
87 public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
88 @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
89 this.client = httpClient;
90 this.oAuthClientService = oAuthClientService;
91 this.apiBridgeConfiguration = apiBridgeConfiguration;
93 programsCache = new ConcurrentHashMap<>();
94 apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
95 communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
96 if (apiRequestHistory != null) {
97 communicationQueue.addAll(apiRequestHistory);
102 * Get all home appliances
104 * @return list of {@link HomeAppliance}
105 * @throws CommunicationException API communication exception
106 * @throws AuthorizationException oAuth authorization exception
108 public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
109 Request request = createRequest(HttpMethod.GET, BASE);
111 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
112 checkResponseCode(HttpStatus.OK_200, request, response, null, null);
114 String responseBody = response.getContentAsString();
115 trackAndLogApiRequest(null, request, null, response, responseBody);
117 return mapToHomeAppliances(responseBody);
118 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
119 logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
120 trackAndLogApiRequest(null, request, null, null, null);
121 throw new CommunicationException(e);
126 * Get home appliance by id
128 * @param haId home appliance id
129 * @return {@link HomeAppliance}
130 * @throws CommunicationException API communication exception
131 * @throws AuthorizationException oAuth authorization exception
133 public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
134 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
136 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
137 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
139 String responseBody = response.getContentAsString();
140 trackAndLogApiRequest(haId, request, null, response, responseBody);
142 return mapToHomeAppliance(responseBody);
143 } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
144 logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
145 trackAndLogApiRequest(haId, request, null, null, null);
146 throw new CommunicationException(e);
151 * Get ambient light state of device.
153 * @param haId home appliance id
154 * @return {@link Data}
155 * @throws CommunicationException API communication exception
156 * @throws AuthorizationException oAuth authorization exception
157 * @throws ApplianceOfflineException appliance is not connected to the cloud
159 public Data getAmbientLightState(String haId)
160 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
161 return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
165 * Set ambient light state of device.
167 * @param haId home appliance id
168 * @param enable enable or disable ambient light
169 * @throws CommunicationException API communication exception
170 * @throws AuthorizationException oAuth authorization exception
171 * @throws ApplianceOfflineException appliance is not connected to the cloud
173 public void setAmbientLightState(String haId, boolean enable)
174 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
175 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
179 * Get functional light state of device.
181 * @param haId home appliance id
182 * @return {@link Data}
183 * @throws CommunicationException API communication exception
184 * @throws AuthorizationException oAuth authorization exception
185 * @throws ApplianceOfflineException appliance is not connected to the cloud
187 public Data getFunctionalLightState(String haId)
188 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
189 return getSetting(haId, SETTING_LIGHTING);
193 * Set functional light state of device.
195 * @param haId home appliance id
196 * @param enable enable or disable functional light
197 * @throws CommunicationException API communication exception
198 * @throws AuthorizationException oAuth authorization exception
199 * @throws ApplianceOfflineException appliance is not connected to the cloud
201 public void setFunctionalLightState(String haId, boolean enable)
202 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
203 putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
207 * Get functional light brightness state of device.
209 * @param haId home appliance id
210 * @return {@link Data}
211 * @throws CommunicationException API communication exception
212 * @throws AuthorizationException oAuth authorization exception
213 * @throws ApplianceOfflineException appliance is not connected to the cloud
215 public Data getFunctionalLightBrightnessState(String haId)
216 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
217 return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
221 * Set functional light brightness of device.
223 * @param haId home appliance id
224 * @param value brightness value 10-100
225 * @throws CommunicationException API communication exception
226 * @throws AuthorizationException oAuth authorization exception
227 * @throws ApplianceOfflineException appliance is not connected to the cloud
229 public void setFunctionalLightBrightnessState(String haId, int value)
230 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
231 putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
235 * Get ambient light brightness state of device.
237 * @param haId home appliance id
238 * @return {@link Data}
239 * @throws CommunicationException API communication exception
240 * @throws AuthorizationException oAuth authorization exception
241 * @throws ApplianceOfflineException appliance is not connected to the cloud
243 public Data getAmbientLightBrightnessState(String haId)
244 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
245 return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
249 * Set ambient light brightness of device.
251 * @param haId home appliance id
252 * @param value brightness value 10-100
253 * @throws CommunicationException API communication exception
254 * @throws AuthorizationException oAuth authorization exception
255 * @throws ApplianceOfflineException appliance is not connected to the cloud
257 public void setAmbientLightBrightnessState(String haId, int value)
258 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
259 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
263 * Get ambient light color state of device.
265 * @param haId home appliance id
266 * @return {@link Data}
267 * @throws CommunicationException API communication exception
268 * @throws AuthorizationException oAuth authorization exception
269 * @throws ApplianceOfflineException appliance is not connected to the cloud
271 public Data getAmbientLightColorState(String haId)
272 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
273 return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
277 * Set ambient light color of device.
279 * @param haId home appliance id
280 * @param value color code
281 * @throws CommunicationException API communication exception
282 * @throws AuthorizationException oAuth authorization exception
283 * @throws ApplianceOfflineException appliance is not connected to the cloud
285 public void setAmbientLightColorState(String haId, String value)
286 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
287 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
291 * Get ambient light custom color state of device.
293 * @param haId home appliance id
294 * @return {@link Data}
295 * @throws CommunicationException API communication exception
296 * @throws AuthorizationException oAuth authorization exception
297 * @throws ApplianceOfflineException appliance is not connected to the cloud
299 public Data getAmbientLightCustomColorState(String haId)
300 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
301 return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
305 * Set ambient light color of device.
307 * @param haId home appliance id
308 * @param value color code
309 * @throws CommunicationException API communication exception
310 * @throws AuthorizationException oAuth authorization exception
311 * @throws ApplianceOfflineException appliance is not connected to the cloud
313 public void setAmbientLightCustomColorState(String haId, String value)
314 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
315 putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
319 * Get power state of device.
321 * @param haId home appliance id
322 * @return {@link Data}
323 * @throws CommunicationException API communication exception
324 * @throws AuthorizationException oAuth authorization exception
325 * @throws ApplianceOfflineException appliance is not connected to the cloud
327 public Data getPowerState(String haId)
328 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
329 return getSetting(haId, SETTING_POWER_STATE);
333 * Set power state of device.
335 * @param haId home appliance id
336 * @param state target state
337 * @throws CommunicationException API communication exception
338 * @throws AuthorizationException oAuth authorization exception
339 * @throws ApplianceOfflineException appliance is not connected to the cloud
341 public void setPowerState(String haId, String state)
342 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
343 putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
347 * Get setpoint temperature of freezer
349 * @param haId home appliance id
350 * @return {@link Data}
351 * @throws CommunicationException API communication exception
352 * @throws AuthorizationException oAuth authorization exception
353 * @throws ApplianceOfflineException appliance is not connected to the cloud
355 public Data getFreezerSetpointTemperature(String haId)
356 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
357 return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
361 * Set setpoint temperature of freezer
363 * @param haId home appliance id
364 * @param state new temperature
365 * @param unit temperature unit
366 * @throws CommunicationException API communication exception
367 * @throws AuthorizationException oAuth authorization exception
368 * @throws ApplianceOfflineException appliance is not connected to the cloud
370 public void setFreezerSetpointTemperature(String haId, String state, String unit)
371 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
372 putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
376 * Get setpoint temperature of fridge
378 * @param haId home appliance id
379 * @return {@link Data} or null in case of communication error
380 * @throws CommunicationException API communication exception
381 * @throws AuthorizationException oAuth authorization exception
382 * @throws ApplianceOfflineException appliance is not connected to the cloud
384 public Data getFridgeSetpointTemperature(String haId)
385 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
386 return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
390 * Set setpoint temperature of fridge
392 * @param haId home appliance id
393 * @param state new temperature
394 * @param unit temperature unit
395 * @throws CommunicationException API communication exception
396 * @throws AuthorizationException oAuth authorization exception
397 * @throws ApplianceOfflineException appliance is not connected to the cloud
399 public void setFridgeSetpointTemperature(String haId, String state, String unit)
400 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
401 putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
405 * Get fridge super mode
407 * @param haId home appliance id
408 * @return {@link Data}
409 * @throws CommunicationException API communication exception
410 * @throws AuthorizationException oAuth authorization exception
411 * @throws ApplianceOfflineException appliance is not connected to the cloud
413 public Data getFridgeSuperMode(String haId)
414 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
415 return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
419 * Set fridge super mode
421 * @param haId home appliance id
422 * @param enable enable or disable fridge super mode
423 * @throws CommunicationException API communication exception
424 * @throws AuthorizationException oAuth authorization exception
425 * @throws ApplianceOfflineException appliance is not connected to the cloud
427 public void setFridgeSuperMode(String haId, boolean enable)
428 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
429 putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
433 * Get freezer super mode
435 * @param haId home appliance id
436 * @return {@link Data}
437 * @throws CommunicationException API communication exception
438 * @throws AuthorizationException oAuth authorization exception
439 * @throws ApplianceOfflineException appliance is not connected to the cloud
441 public Data getFreezerSuperMode(String haId)
442 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
443 return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
447 * Set freezer super mode
449 * @param haId home appliance id
450 * @param enable enable or disable freezer super mode
451 * @throws CommunicationException API communication exception
452 * @throws AuthorizationException oAuth authorization exception
453 * @throws ApplianceOfflineException appliance is not connected to the cloud
455 public void setFreezerSuperMode(String haId, boolean enable)
456 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
457 putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
461 * Get door state of device.
463 * @param haId home appliance id
464 * @return {@link Data}
465 * @throws CommunicationException API communication exception
466 * @throws AuthorizationException oAuth authorization exception
467 * @throws ApplianceOfflineException appliance is not connected to the cloud
469 public Data getDoorState(String haId)
470 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
471 return getStatus(haId, STATUS_DOOR_STATE);
475 * Get operation state of device.
477 * @param haId home appliance id
478 * @return {@link Data}
479 * @throws CommunicationException API communication exception
480 * @throws AuthorizationException oAuth authorization exception
481 * @throws ApplianceOfflineException appliance is not connected to the cloud
483 public Data getOperationState(String haId)
484 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
485 return getStatus(haId, STATUS_OPERATION_STATE);
489 * Get current cavity temperature of oven.
491 * @param haId home appliance id
492 * @return {@link Data}
493 * @throws CommunicationException API communication exception
494 * @throws AuthorizationException oAuth authorization exception
495 * @throws ApplianceOfflineException appliance is not connected to the cloud
497 public Data getCurrentCavityTemperature(String haId)
498 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
499 return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
503 * Is remote start allowed?
505 * @param haId haId home appliance id
506 * @return true or false
507 * @throws CommunicationException API communication exception
508 * @throws AuthorizationException oAuth authorization exception
509 * @throws ApplianceOfflineException appliance is not connected to the cloud
511 public boolean isRemoteControlStartAllowed(String haId)
512 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
513 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
514 return Boolean.parseBoolean(data.getValue());
518 * Is remote control allowed?
520 * @param haId haId home appliance id
521 * @return true or false
522 * @throws CommunicationException API communication exception
523 * @throws AuthorizationException oAuth authorization exception
524 * @throws ApplianceOfflineException appliance is not connected to the cloud
526 public boolean isRemoteControlActive(String haId)
527 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
528 Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
529 return Boolean.parseBoolean(data.getValue());
533 * Is local control allowed?
535 * @param haId haId home appliance id
536 * @return true or false
537 * @throws CommunicationException API communication exception
538 * @throws AuthorizationException oAuth authorization exception
539 * @throws ApplianceOfflineException appliance is not connected to the cloud
541 public boolean isLocalControlActive(String haId)
542 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
543 Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
544 return Boolean.parseBoolean(data.getValue());
548 * Get active program of device.
550 * @param haId home appliance id
551 * @return {@link Program} or null if there is no active program
552 * @throws CommunicationException API communication exception
553 * @throws AuthorizationException oAuth authorization exception
554 * @throws ApplianceOfflineException appliance is not connected to the cloud
556 public @Nullable Program getActiveProgram(String haId)
557 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
558 return getProgram(haId, BASE_PATH + haId + "/programs/active");
562 * Get selected program of device.
564 * @param haId home appliance id
565 * @return {@link Program} or null if there is no selected program
566 * @throws CommunicationException API communication exception
567 * @throws AuthorizationException oAuth authorization exception
568 * @throws ApplianceOfflineException appliance is not connected to the cloud
570 public @Nullable Program getSelectedProgram(String haId)
571 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
572 return getProgram(haId, BASE_PATH + haId + "/programs/selected");
575 public void setSelectedProgram(String haId, String program)
576 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
577 putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
580 public void startProgram(String haId, String program)
581 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
582 putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
585 public void startSelectedProgram(String haId)
586 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
587 String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
588 if (selectedProgram != null) {
589 putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
593 public void startCustomProgram(String haId, String json)
594 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
595 putRaw(haId, BASE_PATH + haId + "/programs/active", json);
598 public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
599 boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
600 String programState = isProgramActive ? "active" : "selected";
602 putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
606 public void stopProgram(String haId)
607 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
608 sendDelete(haId, BASE_PATH + haId + "/programs/active");
611 public List<AvailableProgram> getPrograms(String haId)
612 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
613 List<AvailableProgram> programs;
614 if (programsCache.containsKey(haId)) {
615 logger.debug("Returning cached programs for '{}'.", haId);
616 programs = programsCache.get(haId);
617 programs = programs != null ? programs : Collections.emptyList();
619 programs = getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
620 programsCache.put(haId, programs);
625 public List<AvailableProgram> getAvailablePrograms(String haId)
626 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
627 return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
631 * Get the available options of a program.
633 * @param haId home appliance id
634 * @param programKey program id
635 * @return list of {@link AvailableProgramOption} or null if the program is unsupported by the API
636 * @throws CommunicationException API communication exception
637 * @throws AuthorizationException oAuth authorization exception
638 * @throws ApplianceOfflineException appliance is not connected to the cloud
640 public @Nullable List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
641 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
642 Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
644 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
645 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
647 String responseBody = response.getContentAsString();
648 trackAndLogApiRequest(haId, request, null, response, responseBody);
650 // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram"
651 if (response.getStatus() == HttpStatus.NOT_FOUND_404
652 && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) {
653 throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(),
654 responseBody == null ? "" : responseBody);
657 return response.getStatus() == HttpStatus.OK_200 ? mapToAvailableProgramOption(responseBody, haId) : null;
658 } catch (InterruptedException | TimeoutException | ExecutionException e) {
659 logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
661 trackAndLogApiRequest(haId, request, null, null, null);
662 throw new CommunicationException(e);
667 * Get latest API requests.
669 * @return communication queue
671 public Collection<ApiRequest> getLatestApiRequests() {
672 return communicationQueue.getAll();
675 private Data getSetting(String haId, String setting)
676 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
677 return getData(haId, BASE_PATH + haId + "/settings/" + setting);
680 private void putSettings(String haId, Data data)
681 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
682 putSettings(haId, data, VALUE_TYPE_STRING);
685 private void putSettings(String haId, Data data, int valueType)
686 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
687 putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
690 private Data getStatus(String haId, String status)
691 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
692 return getData(haId, BASE_PATH + haId + "/status/" + status);
695 public @Nullable String getRaw(String haId, String path)
696 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
697 return getRaw(haId, path, false);
700 public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
701 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
702 Request request = createRequest(HttpMethod.GET, path);
704 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
705 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
707 String responseBody = response.getContentAsString();
708 trackAndLogApiRequest(haId, request, null, response, responseBody);
710 if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
713 } catch (InterruptedException | TimeoutException | ExecutionException e) {
714 logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
715 trackAndLogApiRequest(haId, request, null, null, null);
716 throw new CommunicationException(e);
721 public String putRaw(String haId, String path, String requestBodyPayload)
722 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
723 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
726 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
727 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
729 String responseBody = response.getContentAsString();
730 trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
732 } catch (InterruptedException | TimeoutException | ExecutionException e) {
733 logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
735 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
736 throw new CommunicationException(e);
740 private @Nullable Program getProgram(String haId, String path)
741 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
742 Request request = createRequest(HttpMethod.GET, path);
744 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
745 checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
747 String responseBody = response.getContentAsString();
748 trackAndLogApiRequest(haId, request, null, response, responseBody);
750 if (response.getStatus() == HttpStatus.OK_200) {
751 return mapToProgram(responseBody);
753 } catch (InterruptedException | TimeoutException | ExecutionException e) {
754 logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
755 trackAndLogApiRequest(haId, request, null, null, null);
756 throw new CommunicationException(e);
761 private List<AvailableProgram> getAvailablePrograms(String haId, String path)
762 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
763 Request request = createRequest(HttpMethod.GET, path);
765 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
766 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
768 String responseBody = response.getContentAsString();
769 trackAndLogApiRequest(haId, request, null, response, responseBody);
771 return mapToAvailablePrograms(responseBody, haId);
772 } catch (InterruptedException | TimeoutException | ExecutionException e) {
773 logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
774 trackAndLogApiRequest(haId, request, null, null, null);
775 throw new CommunicationException(e);
779 private void sendDelete(String haId, String path)
780 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
781 Request request = createRequest(HttpMethod.DELETE, path);
783 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
784 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
786 trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
787 } catch (InterruptedException | TimeoutException | ExecutionException e) {
788 logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
789 trackAndLogApiRequest(haId, request, null, null, null);
790 throw new CommunicationException(e);
794 private Data getData(String haId, String path)
795 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
796 Request request = createRequest(HttpMethod.GET, path);
798 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
799 checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
801 String responseBody = response.getContentAsString();
802 trackAndLogApiRequest(haId, request, null, response, responseBody);
804 return mapToState(responseBody);
805 } catch (InterruptedException | TimeoutException | ExecutionException e) {
806 logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
807 trackAndLogApiRequest(haId, request, null, null, null);
808 throw new CommunicationException(e);
812 private void putData(String haId, String path, Data data, int valueType)
813 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
814 JsonObject innerObject = new JsonObject();
815 innerObject.addProperty("key", data.getName());
817 if (data.getValue() != null) {
818 if (valueType == VALUE_TYPE_INT) {
819 innerObject.addProperty("value", data.getValueAsInt());
820 } else if (valueType == VALUE_TYPE_BOOLEAN) {
821 innerObject.addProperty("value", data.getValueAsBoolean());
823 innerObject.addProperty("value", data.getValue());
827 if (data.getUnit() != null) {
828 innerObject.addProperty("unit", data.getUnit());
831 JsonObject dataObject = new JsonObject();
832 dataObject.add("data", innerObject);
833 String requestBodyPayload = dataObject.toString();
835 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
838 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
839 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
841 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
842 } catch (InterruptedException | TimeoutException | ExecutionException e) {
843 logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
844 valueType, e.getMessage());
845 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
846 throw new CommunicationException(e);
850 private void putOption(String haId, String path, Option option, boolean asInt)
851 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
852 JsonObject innerObject = new JsonObject();
853 innerObject.addProperty("key", option.getKey());
855 if (option.getValue() != null) {
857 innerObject.addProperty("value", option.getValueAsInt());
859 innerObject.addProperty("value", option.getValue());
863 if (option.getUnit() != null) {
864 innerObject.addProperty("unit", option.getUnit());
867 JsonArray optionsArray = new JsonArray();
868 optionsArray.add(innerObject);
870 JsonObject optionsObject = new JsonObject();
871 optionsObject.add("options", optionsArray);
873 JsonObject dataObject = new JsonObject();
874 dataObject.add("data", optionsObject);
876 String requestBodyPayload = dataObject.toString();
878 Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
881 ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
882 checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
884 trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
885 } catch (InterruptedException | TimeoutException | ExecutionException e) {
886 logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
887 asInt, e.getMessage());
888 trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
889 throw new CommunicationException(e);
893 private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
894 @Nullable String requestPayload)
895 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
896 checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload);
899 private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
900 @Nullable String haId, @Nullable String requestPayload)
901 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
902 if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
903 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
904 logger.debug("Current access token is invalid.");
905 String responseBody = response.getContentAsString();
906 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
907 throw new AuthorizationException("Token invalid!");
910 if (!desiredCodes.contains(response.getStatus())) {
911 int code = response.getStatus();
912 String message = response.getReason();
914 logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
915 String responseBody = response.getContentAsString();
916 trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
918 responseBody = responseBody == null ? "" : responseBody;
919 if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
920 && responseBody.toLowerCase().contains("offline")) {
921 throw new ApplianceOfflineException(code, message, responseBody);
923 throw new CommunicationException(code, message, responseBody);
928 private Program mapToProgram(String json) {
929 ArrayList<Option> optionList = new ArrayList<>();
930 JsonObject responseObject = parseString(json).getAsJsonObject();
931 JsonObject data = responseObject.getAsJsonObject("data");
932 Program result = new Program(data.get("key").getAsString(), optionList);
933 JsonArray options = data.getAsJsonArray("options");
935 options.forEach(option -> {
936 JsonObject obj = (JsonObject) option;
939 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
941 String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
944 String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
946 optionList.add(new Option(key, value, unit));
952 private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
953 ArrayList<AvailableProgram> result = new ArrayList<>();
956 JsonObject responseObject = parseString(json).getAsJsonObject();
958 JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
959 programs.forEach(program -> {
960 JsonObject obj = (JsonObject) program;
962 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
963 JsonObject constraints = obj.getAsJsonObject("constraints");
964 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
966 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
969 if (key != null && execution != null) {
970 result.add(new AvailableProgram(key, available, execution));
973 } catch (Exception e) {
974 logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
980 private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
981 ArrayList<AvailableProgramOption> result = new ArrayList<>();
984 JsonObject responseObject = parseString(json).getAsJsonObject();
986 JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
987 options.forEach(option -> {
988 JsonObject obj = (JsonObject) option;
990 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
991 ArrayList<String> allowedValues = new ArrayList<>();
992 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
993 .forEach(value -> allowedValues.add(value.getAsString()));
996 result.add(new AvailableProgramOption(key, allowedValues));
999 } catch (Exception e) {
1000 logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
1006 private HomeAppliance mapToHomeAppliance(String json) {
1007 JsonObject responseObject = parseString(json).getAsJsonObject();
1009 JsonObject data = responseObject.getAsJsonObject("data");
1011 return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
1012 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
1013 data.get("type").getAsString(), data.get("enumber").getAsString());
1016 private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1017 final ArrayList<HomeAppliance> result = new ArrayList<>();
1018 JsonObject responseObject = parseString(json).getAsJsonObject();
1020 JsonObject data = responseObject.getAsJsonObject("data");
1021 JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1023 homeappliances.forEach(appliance -> {
1024 JsonObject obj = (JsonObject) appliance;
1026 result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1027 obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1028 obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1034 private Data mapToState(String json) {
1035 JsonObject responseObject = parseString(json).getAsJsonObject();
1037 JsonObject data = responseObject.getAsJsonObject("data");
1040 String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1042 return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1045 private Request createRequest(HttpMethod method, String path)
1046 throws AuthorizationException, CommunicationException {
1047 return client.newRequest(apiUrl + path)
1048 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1049 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1052 private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1053 @Nullable ContentResponse response, @Nullable String responseBody) {
1054 HomeConnectRequest homeConnectRequest = map(request, requestBody);
1056 HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1058 logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1059 trackApiRequest(homeConnectRequest, homeConnectResponse);
1062 private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1063 @Nullable HomeConnectResponse homeConnectResponse) {
1064 if (logger.isDebugEnabled()) {
1065 StringBuilder sb = new StringBuilder();
1068 sb.append("[").append(haId).append("] ");
1071 sb.append(homeConnectRequest.getMethod()).append(" ");
1072 if (homeConnectResponse != null) {
1073 sb.append(homeConnectResponse.getCode()).append(" ");
1075 sb.append(homeConnectRequest.getUrl()).append("\n");
1076 homeConnectRequest.getHeader()
1077 .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1079 if (homeConnectRequest.getBody() != null) {
1080 sb.append(homeConnectRequest.getBody()).append("\n");
1083 if (homeConnectResponse != null) {
1085 homeConnectResponse.getHeader()
1086 .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1088 if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1089 sb.append(homeConnectResponse.getBody()).append("\n");
1092 logger.debug("{}", sb.toString());
1096 private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1097 @Nullable HomeConnectResponse homeConnectResponse) {
1098 communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1101 private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1102 Map<String, String> headers = new HashMap<>();
1103 request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1105 return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1106 requestBody != null ? formatJsonBody(requestBody) : null);
1109 private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1110 Map<String, String> headers = new HashMap<>();
1111 response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1113 return new HomeConnectResponse(response.getStatus(), headers,
1114 responseBody != null ? formatJsonBody(responseBody) : null);