]> git.basschouten.com Git - openhab-addons.git/blob
e8fa122ef932376b89973d64283661705a7d1e6f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.homeconnect.internal.client;
14
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.*;
18
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
28
29 import javax.ws.rs.core.HttpHeaders;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.util.StringContentProvider;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.http.HttpStatus;
39 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
40 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
41 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
42 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
43 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
44 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
45 import org.openhab.binding.homeconnect.internal.client.model.Data;
46 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
47 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
48 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
49 import org.openhab.binding.homeconnect.internal.client.model.Option;
50 import org.openhab.binding.homeconnect.internal.client.model.Program;
51 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
52 import org.openhab.core.auth.client.oauth2.OAuthClientService;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import com.google.gson.JsonArray;
57 import com.google.gson.JsonObject;
58
59 /**
60  * Client for Home Connect API.
61  *
62  * @author Jonas BrĂ¼stel - Initial contribution
63  * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
64  *
65  */
66 @NonNullByDefault
67 public class HomeConnectApiClient {
68     private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
69     private static final String BASE = "/api/homeappliances";
70     private static final String BASE_PATH = BASE + "/";
71     private static final int REQUEST_TIMEOUT_SEC = 30;
72     private static final int VALUE_TYPE_STRING = 0;
73     private static final int VALUE_TYPE_INT = 1;
74     private static final int VALUE_TYPE_BOOLEAN = 2;
75     private static final int COMMUNICATION_QUEUE_SIZE = 50;
76
77     private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
78     private final HttpClient client;
79     private final String apiUrl;
80     private final OAuthClientService oAuthClientService;
81     private final CircularQueue<ApiRequest> communicationQueue;
82     private final ApiBridgeConfiguration apiBridgeConfiguration;
83
84     public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
85             @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
86         this.client = httpClient;
87         this.oAuthClientService = oAuthClientService;
88         this.apiBridgeConfiguration = apiBridgeConfiguration;
89
90         apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
91         communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
92         if (apiRequestHistory != null) {
93             communicationQueue.addAll(apiRequestHistory);
94         }
95     }
96
97     /**
98      * Get all home appliances
99      *
100      * @return list of {@link HomeAppliance}
101      * @throws CommunicationException API communication exception
102      * @throws AuthorizationException oAuth authorization exception
103      */
104     public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
105         Request request = createRequest(HttpMethod.GET, BASE);
106         try {
107             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
108             checkResponseCode(HttpStatus.OK_200, request, response, null, null);
109
110             String responseBody = response.getContentAsString();
111             trackAndLogApiRequest(null, request, null, response, responseBody);
112
113             return mapToHomeAppliances(responseBody);
114         } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
115             logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
116             trackAndLogApiRequest(null, request, null, null, null);
117             throw new CommunicationException(e);
118         }
119     }
120
121     /**
122      * Get home appliance by id
123      *
124      * @param haId home appliance id
125      * @return {@link HomeAppliance}
126      * @throws CommunicationException API communication exception
127      * @throws AuthorizationException oAuth authorization exception
128      */
129     public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
130         Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
131         try {
132             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
133             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
134
135             String responseBody = response.getContentAsString();
136             trackAndLogApiRequest(haId, request, null, response, responseBody);
137
138             return mapToHomeAppliance(responseBody);
139         } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
140             logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
141             trackAndLogApiRequest(haId, request, null, null, null);
142             throw new CommunicationException(e);
143         }
144     }
145
146     /**
147      * Get ambient light state of device.
148      *
149      * @param haId home appliance id
150      * @return {@link Data}
151      * @throws CommunicationException API communication exception
152      * @throws AuthorizationException oAuth authorization exception
153      * @throws ApplianceOfflineException appliance is not connected to the cloud
154      */
155     public Data getAmbientLightState(String haId)
156             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
157         return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
158     }
159
160     /**
161      * Set ambient light state of device.
162      *
163      * @param haId home appliance id
164      * @param enable enable or disable ambient light
165      * @throws CommunicationException API communication exception
166      * @throws AuthorizationException oAuth authorization exception
167      * @throws ApplianceOfflineException appliance is not connected to the cloud
168      */
169     public void setAmbientLightState(String haId, boolean enable)
170             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
171         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
172     }
173
174     /**
175      * Get functional light state of device.
176      *
177      * @param haId home appliance id
178      * @return {@link Data}
179      * @throws CommunicationException API communication exception
180      * @throws AuthorizationException oAuth authorization exception
181      * @throws ApplianceOfflineException appliance is not connected to the cloud
182      */
183     public Data getFunctionalLightState(String haId)
184             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
185         return getSetting(haId, SETTING_LIGHTING);
186     }
187
188     /**
189      * Set functional light state of device.
190      *
191      * @param haId home appliance id
192      * @param enable enable or disable functional light
193      * @throws CommunicationException API communication exception
194      * @throws AuthorizationException oAuth authorization exception
195      * @throws ApplianceOfflineException appliance is not connected to the cloud
196      */
197     public void setFunctionalLightState(String haId, boolean enable)
198             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
199         putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
200     }
201
202     /**
203      * Get functional light brightness state of device.
204      *
205      * @param haId home appliance id
206      * @return {@link Data}
207      * @throws CommunicationException API communication exception
208      * @throws AuthorizationException oAuth authorization exception
209      * @throws ApplianceOfflineException appliance is not connected to the cloud
210      */
211     public Data getFunctionalLightBrightnessState(String haId)
212             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
213         return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
214     }
215
216     /**
217      * Set functional light brightness of device.
218      *
219      * @param haId home appliance id
220      * @param value brightness value 10-100
221      * @throws CommunicationException API communication exception
222      * @throws AuthorizationException oAuth authorization exception
223      * @throws ApplianceOfflineException appliance is not connected to the cloud
224      */
225     public void setFunctionalLightBrightnessState(String haId, int value)
226             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
227         putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
228     }
229
230     /**
231      * Get ambient light brightness state of device.
232      *
233      * @param haId home appliance id
234      * @return {@link Data}
235      * @throws CommunicationException API communication exception
236      * @throws AuthorizationException oAuth authorization exception
237      * @throws ApplianceOfflineException appliance is not connected to the cloud
238      */
239     public Data getAmbientLightBrightnessState(String haId)
240             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
241         return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
242     }
243
244     /**
245      * Set ambient light brightness of device.
246      *
247      * @param haId home appliance id
248      * @param value brightness value 10-100
249      * @throws CommunicationException API communication exception
250      * @throws AuthorizationException oAuth authorization exception
251      * @throws ApplianceOfflineException appliance is not connected to the cloud
252      */
253     public void setAmbientLightBrightnessState(String haId, int value)
254             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
255         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
256     }
257
258     /**
259      * Get ambient light color state of device.
260      *
261      * @param haId home appliance id
262      * @return {@link Data}
263      * @throws CommunicationException API communication exception
264      * @throws AuthorizationException oAuth authorization exception
265      * @throws ApplianceOfflineException appliance is not connected to the cloud
266      */
267     public Data getAmbientLightColorState(String haId)
268             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
269         return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
270     }
271
272     /**
273      * Set ambient light color of device.
274      *
275      * @param haId home appliance id
276      * @param value color code
277      * @throws CommunicationException API communication exception
278      * @throws AuthorizationException oAuth authorization exception
279      * @throws ApplianceOfflineException appliance is not connected to the cloud
280      */
281     public void setAmbientLightColorState(String haId, String value)
282             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
283         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
284     }
285
286     /**
287      * Get ambient light custom color state of device.
288      *
289      * @param haId home appliance id
290      * @return {@link Data}
291      * @throws CommunicationException API communication exception
292      * @throws AuthorizationException oAuth authorization exception
293      * @throws ApplianceOfflineException appliance is not connected to the cloud
294      */
295     public Data getAmbientLightCustomColorState(String haId)
296             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
297         return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
298     }
299
300     /**
301      * Set ambient light color of device.
302      *
303      * @param haId home appliance id
304      * @param value color code
305      * @throws CommunicationException API communication exception
306      * @throws AuthorizationException oAuth authorization exception
307      * @throws ApplianceOfflineException appliance is not connected to the cloud
308      */
309     public void setAmbientLightCustomColorState(String haId, String value)
310             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
311         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
312     }
313
314     /**
315      * Get power state of device.
316      *
317      * @param haId home appliance id
318      * @return {@link Data}
319      * @throws CommunicationException API communication exception
320      * @throws AuthorizationException oAuth authorization exception
321      * @throws ApplianceOfflineException appliance is not connected to the cloud
322      */
323     public Data getPowerState(String haId)
324             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
325         return getSetting(haId, SETTING_POWER_STATE);
326     }
327
328     /**
329      * Set power state of device.
330      *
331      * @param haId home appliance id
332      * @param state target state
333      * @throws CommunicationException API communication exception
334      * @throws AuthorizationException oAuth authorization exception
335      * @throws ApplianceOfflineException appliance is not connected to the cloud
336      */
337     public void setPowerState(String haId, String state)
338             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
339         putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
340     }
341
342     /**
343      * Get setpoint temperature of freezer
344      *
345      * @param haId home appliance id
346      * @return {@link Data}
347      * @throws CommunicationException API communication exception
348      * @throws AuthorizationException oAuth authorization exception
349      * @throws ApplianceOfflineException appliance is not connected to the cloud
350      */
351     public Data getFreezerSetpointTemperature(String haId)
352             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
353         return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
354     }
355
356     /**
357      * Set setpoint temperature of freezer
358      *
359      * @param haId home appliance id
360      * @param state new temperature
361      * @param unit temperature unit
362      * @throws CommunicationException API communication exception
363      * @throws AuthorizationException oAuth authorization exception
364      * @throws ApplianceOfflineException appliance is not connected to the cloud
365      */
366     public void setFreezerSetpointTemperature(String haId, String state, String unit)
367             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
368         putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
369     }
370
371     /**
372      * Get setpoint temperature of fridge
373      *
374      * @param haId home appliance id
375      * @return {@link Data} or null in case of communication error
376      * @throws CommunicationException API communication exception
377      * @throws AuthorizationException oAuth authorization exception
378      * @throws ApplianceOfflineException appliance is not connected to the cloud
379      */
380     public Data getFridgeSetpointTemperature(String haId)
381             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
382         return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
383     }
384
385     /**
386      * Set setpoint temperature of fridge
387      *
388      * @param haId home appliance id
389      * @param state new temperature
390      * @param unit temperature unit
391      * @throws CommunicationException API communication exception
392      * @throws AuthorizationException oAuth authorization exception
393      * @throws ApplianceOfflineException appliance is not connected to the cloud
394      */
395     public void setFridgeSetpointTemperature(String haId, String state, String unit)
396             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
397         putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
398     }
399
400     /**
401      * Get fridge super mode
402      *
403      * @param haId home appliance id
404      * @return {@link Data}
405      * @throws CommunicationException API communication exception
406      * @throws AuthorizationException oAuth authorization exception
407      * @throws ApplianceOfflineException appliance is not connected to the cloud
408      */
409     public Data getFridgeSuperMode(String haId)
410             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
411         return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
412     }
413
414     /**
415      * Set fridge super mode
416      *
417      * @param haId home appliance id
418      * @param enable enable or disable fridge super mode
419      * @throws CommunicationException API communication exception
420      * @throws AuthorizationException oAuth authorization exception
421      * @throws ApplianceOfflineException appliance is not connected to the cloud
422      */
423     public void setFridgeSuperMode(String haId, boolean enable)
424             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
425         putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
426     }
427
428     /**
429      * Get freezer super mode
430      *
431      * @param haId home appliance id
432      * @return {@link Data}
433      * @throws CommunicationException API communication exception
434      * @throws AuthorizationException oAuth authorization exception
435      * @throws ApplianceOfflineException appliance is not connected to the cloud
436      */
437     public Data getFreezerSuperMode(String haId)
438             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
439         return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
440     }
441
442     /**
443      * Set freezer super mode
444      *
445      * @param haId home appliance id
446      * @param enable enable or disable freezer super mode
447      * @throws CommunicationException API communication exception
448      * @throws AuthorizationException oAuth authorization exception
449      * @throws ApplianceOfflineException appliance is not connected to the cloud
450      */
451     public void setFreezerSuperMode(String haId, boolean enable)
452             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
453         putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
454     }
455
456     /**
457      * Get door state of device.
458      *
459      * @param haId home appliance id
460      * @return {@link Data}
461      * @throws CommunicationException API communication exception
462      * @throws AuthorizationException oAuth authorization exception
463      * @throws ApplianceOfflineException appliance is not connected to the cloud
464      */
465     public Data getDoorState(String haId)
466             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
467         return getStatus(haId, STATUS_DOOR_STATE);
468     }
469
470     /**
471      * Get operation state of device.
472      *
473      * @param haId home appliance id
474      * @return {@link Data}
475      * @throws CommunicationException API communication exception
476      * @throws AuthorizationException oAuth authorization exception
477      * @throws ApplianceOfflineException appliance is not connected to the cloud
478      */
479     public Data getOperationState(String haId)
480             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
481         return getStatus(haId, STATUS_OPERATION_STATE);
482     }
483
484     /**
485      * Get current cavity temperature of oven.
486      *
487      * @param haId home appliance id
488      * @return {@link Data}
489      * @throws CommunicationException API communication exception
490      * @throws AuthorizationException oAuth authorization exception
491      * @throws ApplianceOfflineException appliance is not connected to the cloud
492      */
493     public Data getCurrentCavityTemperature(String haId)
494             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
495         return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
496     }
497
498     /**
499      * Is remote start allowed?
500      *
501      * @param haId haId home appliance id
502      * @return true or false
503      * @throws CommunicationException API communication exception
504      * @throws AuthorizationException oAuth authorization exception
505      * @throws ApplianceOfflineException appliance is not connected to the cloud
506      */
507     public boolean isRemoteControlStartAllowed(String haId)
508             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
509         Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
510         return Boolean.parseBoolean(data.getValue());
511     }
512
513     /**
514      * Is remote control allowed?
515      *
516      * @param haId haId home appliance id
517      * @return true or false
518      * @throws CommunicationException API communication exception
519      * @throws AuthorizationException oAuth authorization exception
520      * @throws ApplianceOfflineException appliance is not connected to the cloud
521      */
522     public boolean isRemoteControlActive(String haId)
523             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
524         Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
525         return Boolean.parseBoolean(data.getValue());
526     }
527
528     /**
529      * Is local control allowed?
530      *
531      * @param haId haId home appliance id
532      * @return true or false
533      * @throws CommunicationException API communication exception
534      * @throws AuthorizationException oAuth authorization exception
535      * @throws ApplianceOfflineException appliance is not connected to the cloud
536      */
537     public boolean isLocalControlActive(String haId)
538             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
539         Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
540         return Boolean.parseBoolean(data.getValue());
541     }
542
543     /**
544      * Get active program of device.
545      *
546      * @param haId home appliance id
547      * @return {@link Program} or null if there is no active program
548      * @throws CommunicationException API communication exception
549      * @throws AuthorizationException oAuth authorization exception
550      * @throws ApplianceOfflineException appliance is not connected to the cloud
551      */
552     public @Nullable Program getActiveProgram(String haId)
553             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
554         return getProgram(haId, BASE_PATH + haId + "/programs/active");
555     }
556
557     /**
558      * Get selected program of device.
559      *
560      * @param haId home appliance id
561      * @return {@link Program} or null if there is no selected program
562      * @throws CommunicationException API communication exception
563      * @throws AuthorizationException oAuth authorization exception
564      * @throws ApplianceOfflineException appliance is not connected to the cloud
565      */
566     public @Nullable Program getSelectedProgram(String haId)
567             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
568         return getProgram(haId, BASE_PATH + haId + "/programs/selected");
569     }
570
571     public void setSelectedProgram(String haId, String program)
572             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
573         putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
574     }
575
576     public void startProgram(String haId, String program)
577             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
578         putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
579     }
580
581     public void startSelectedProgram(String haId)
582             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
583         String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
584         if (selectedProgram != null) {
585             putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
586         }
587     }
588
589     public void startCustomProgram(String haId, String json)
590             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
591         putRaw(haId, BASE_PATH + haId + "/programs/active", json);
592     }
593
594     public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
595             boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
596         String programState = isProgramActive ? "active" : "selected";
597
598         putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
599                 valueAsInt);
600     }
601
602     public void stopProgram(String haId)
603             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
604         sendDelete(haId, BASE_PATH + haId + "/programs/active");
605     }
606
607     public List<AvailableProgram> getPrograms(String haId)
608             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
609         return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
610     }
611
612     public List<AvailableProgram> getAvailablePrograms(String haId)
613             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
614         return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
615     }
616
617     /**
618      * Get the available options of a program.
619      *
620      * @param haId home appliance id
621      * @param programKey program id
622      * @return list of {@link AvailableProgramOption} or null if the program is unsupported by the API
623      * @throws CommunicationException API communication exception
624      * @throws AuthorizationException oAuth authorization exception
625      * @throws ApplianceOfflineException appliance is not connected to the cloud
626      */
627     public @Nullable List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
628             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
629         Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
630         try {
631             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
632             checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
633
634             String responseBody = response.getContentAsString();
635             trackAndLogApiRequest(haId, request, null, response, responseBody);
636
637             // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram"
638             if (response.getStatus() == HttpStatus.NOT_FOUND_404
639                     && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) {
640                 throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(),
641                         responseBody == null ? "" : responseBody);
642             }
643
644             return response.getStatus() == HttpStatus.OK_200 ? mapToAvailableProgramOption(responseBody, haId) : null;
645         } catch (InterruptedException | TimeoutException | ExecutionException e) {
646             logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
647                     e.getMessage());
648             trackAndLogApiRequest(haId, request, null, null, null);
649             throw new CommunicationException(e);
650         }
651     }
652
653     /**
654      * Get latest API requests.
655      *
656      * @return communication queue
657      */
658     public Collection<ApiRequest> getLatestApiRequests() {
659         return communicationQueue.getAll();
660     }
661
662     private Data getSetting(String haId, String setting)
663             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
664         return getData(haId, BASE_PATH + haId + "/settings/" + setting);
665     }
666
667     private void putSettings(String haId, Data data)
668             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
669         putSettings(haId, data, VALUE_TYPE_STRING);
670     }
671
672     private void putSettings(String haId, Data data, int valueType)
673             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
674         putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
675     }
676
677     private Data getStatus(String haId, String status)
678             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
679         return getData(haId, BASE_PATH + haId + "/status/" + status);
680     }
681
682     public @Nullable String getRaw(String haId, String path)
683             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
684         return getRaw(haId, path, false);
685     }
686
687     public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
688             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
689         Request request = createRequest(HttpMethod.GET, path);
690         try {
691             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
692             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
693
694             String responseBody = response.getContentAsString();
695             trackAndLogApiRequest(haId, request, null, response, responseBody);
696
697             if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
698                 return responseBody;
699             }
700         } catch (InterruptedException | TimeoutException | ExecutionException e) {
701             logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
702             trackAndLogApiRequest(haId, request, null, null, null);
703             throw new CommunicationException(e);
704         }
705         return null;
706     }
707
708     public String putRaw(String haId, String path, String requestBodyPayload)
709             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
710         Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
711                 BSH_JSON_V1);
712         try {
713             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
714             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
715
716             String responseBody = response.getContentAsString();
717             trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
718             return responseBody;
719         } catch (InterruptedException | TimeoutException | ExecutionException e) {
720             logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
721                     e.getMessage());
722             trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
723             throw new CommunicationException(e);
724         }
725     }
726
727     private @Nullable Program getProgram(String haId, String path)
728             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
729         Request request = createRequest(HttpMethod.GET, path);
730         try {
731             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
732             checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
733
734             String responseBody = response.getContentAsString();
735             trackAndLogApiRequest(haId, request, null, response, responseBody);
736
737             if (response.getStatus() == HttpStatus.OK_200) {
738                 return mapToProgram(responseBody);
739             }
740         } catch (InterruptedException | TimeoutException | ExecutionException e) {
741             logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
742             trackAndLogApiRequest(haId, request, null, null, null);
743             throw new CommunicationException(e);
744         }
745         return null;
746     }
747
748     private List<AvailableProgram> getAvailablePrograms(String haId, String path)
749             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
750         Request request = createRequest(HttpMethod.GET, path);
751         try {
752             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
753             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
754
755             String responseBody = response.getContentAsString();
756             trackAndLogApiRequest(haId, request, null, response, responseBody);
757
758             return mapToAvailablePrograms(responseBody, haId);
759         } catch (InterruptedException | TimeoutException | ExecutionException e) {
760             logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
761             trackAndLogApiRequest(haId, request, null, null, null);
762             throw new CommunicationException(e);
763         }
764     }
765
766     private void sendDelete(String haId, String path)
767             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
768         Request request = createRequest(HttpMethod.DELETE, path);
769         try {
770             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
771             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
772
773             trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
774         } catch (InterruptedException | TimeoutException | ExecutionException e) {
775             logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
776             trackAndLogApiRequest(haId, request, null, null, null);
777             throw new CommunicationException(e);
778         }
779     }
780
781     private Data getData(String haId, String path)
782             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
783         Request request = createRequest(HttpMethod.GET, path);
784         try {
785             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
786             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
787
788             String responseBody = response.getContentAsString();
789             trackAndLogApiRequest(haId, request, null, response, responseBody);
790
791             return mapToState(responseBody);
792         } catch (InterruptedException | TimeoutException | ExecutionException e) {
793             logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
794             trackAndLogApiRequest(haId, request, null, null, null);
795             throw new CommunicationException(e);
796         }
797     }
798
799     private void putData(String haId, String path, Data data, int valueType)
800             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
801         JsonObject innerObject = new JsonObject();
802         innerObject.addProperty("key", data.getName());
803
804         if (data.getValue() != null) {
805             if (valueType == VALUE_TYPE_INT) {
806                 innerObject.addProperty("value", data.getValueAsInt());
807             } else if (valueType == VALUE_TYPE_BOOLEAN) {
808                 innerObject.addProperty("value", data.getValueAsBoolean());
809             } else {
810                 innerObject.addProperty("value", data.getValue());
811             }
812         }
813
814         if (data.getUnit() != null) {
815             innerObject.addProperty("unit", data.getUnit());
816         }
817
818         JsonObject dataObject = new JsonObject();
819         dataObject.add("data", innerObject);
820         String requestBodyPayload = dataObject.toString();
821
822         Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
823                 BSH_JSON_V1);
824         try {
825             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
826             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
827
828             trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
829         } catch (InterruptedException | TimeoutException | ExecutionException e) {
830             logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
831                     valueType, e.getMessage());
832             trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
833             throw new CommunicationException(e);
834         }
835     }
836
837     private void putOption(String haId, String path, Option option, boolean asInt)
838             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
839         JsonObject innerObject = new JsonObject();
840         innerObject.addProperty("key", option.getKey());
841
842         if (option.getValue() != null) {
843             if (asInt) {
844                 innerObject.addProperty("value", option.getValueAsInt());
845             } else {
846                 innerObject.addProperty("value", option.getValue());
847             }
848         }
849
850         if (option.getUnit() != null) {
851             innerObject.addProperty("unit", option.getUnit());
852         }
853
854         JsonArray optionsArray = new JsonArray();
855         optionsArray.add(innerObject);
856
857         JsonObject optionsObject = new JsonObject();
858         optionsObject.add("options", optionsArray);
859
860         JsonObject dataObject = new JsonObject();
861         dataObject.add("data", optionsObject);
862
863         String requestBodyPayload = dataObject.toString();
864
865         Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
866                 BSH_JSON_V1);
867         try {
868             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
869             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
870
871             trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
872         } catch (InterruptedException | TimeoutException | ExecutionException e) {
873             logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
874                     asInt, e.getMessage());
875             trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
876             throw new CommunicationException(e);
877         }
878     }
879
880     private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
881             @Nullable String requestPayload)
882             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
883         checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload);
884     }
885
886     private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
887             @Nullable String haId, @Nullable String requestPayload)
888             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
889         if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
890                 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
891             logger.debug("Current access token is invalid.");
892             String responseBody = response.getContentAsString();
893             trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
894             throw new AuthorizationException("Token invalid!");
895         }
896
897         if (!desiredCodes.contains(response.getStatus())) {
898             int code = response.getStatus();
899             String message = response.getReason();
900
901             logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
902             String responseBody = response.getContentAsString();
903             trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
904
905             responseBody = responseBody == null ? "" : responseBody;
906             if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
907                     && responseBody.toLowerCase().contains("offline")) {
908                 throw new ApplianceOfflineException(code, message, responseBody);
909             } else {
910                 throw new CommunicationException(code, message, responseBody);
911             }
912         }
913     }
914
915     private Program mapToProgram(String json) {
916         ArrayList<Option> optionList = new ArrayList<>();
917         JsonObject responseObject = parseString(json).getAsJsonObject();
918         JsonObject data = responseObject.getAsJsonObject("data");
919         Program result = new Program(data.get("key").getAsString(), optionList);
920         JsonArray options = data.getAsJsonArray("options");
921
922         options.forEach(option -> {
923             JsonObject obj = (JsonObject) option;
924
925             @Nullable
926             String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
927             @Nullable
928             String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
929                     : null;
930             @Nullable
931             String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
932
933             optionList.add(new Option(key, value, unit));
934         });
935
936         return result;
937     }
938
939     private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
940         ArrayList<AvailableProgram> result = new ArrayList<>();
941
942         try {
943             JsonObject responseObject = parseString(json).getAsJsonObject();
944
945             JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
946             programs.forEach(program -> {
947                 JsonObject obj = (JsonObject) program;
948                 @Nullable
949                 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
950                 JsonObject constraints = obj.getAsJsonObject("constraints");
951                 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
952                 @Nullable
953                 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
954                         : null;
955
956                 if (key != null && execution != null) {
957                     result.add(new AvailableProgram(key, available, execution));
958                 }
959             });
960         } catch (Exception e) {
961             logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
962         }
963
964         return result;
965     }
966
967     private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
968         ArrayList<AvailableProgramOption> result = new ArrayList<>();
969
970         try {
971             JsonObject responseObject = parseString(json).getAsJsonObject();
972
973             JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
974             options.forEach(option -> {
975                 JsonObject obj = (JsonObject) option;
976                 @Nullable
977                 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
978                 ArrayList<String> allowedValues = new ArrayList<>();
979                 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
980                         .forEach(value -> allowedValues.add(value.getAsString()));
981
982                 if (key != null) {
983                     result.add(new AvailableProgramOption(key, allowedValues));
984                 }
985             });
986         } catch (Exception e) {
987             logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
988         }
989
990         return result;
991     }
992
993     private HomeAppliance mapToHomeAppliance(String json) {
994         JsonObject responseObject = parseString(json).getAsJsonObject();
995
996         JsonObject data = responseObject.getAsJsonObject("data");
997
998         return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
999                 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
1000                 data.get("type").getAsString(), data.get("enumber").getAsString());
1001     }
1002
1003     private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1004         final ArrayList<HomeAppliance> result = new ArrayList<>();
1005         JsonObject responseObject = parseString(json).getAsJsonObject();
1006
1007         JsonObject data = responseObject.getAsJsonObject("data");
1008         JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1009
1010         homeappliances.forEach(appliance -> {
1011             JsonObject obj = (JsonObject) appliance;
1012
1013             result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1014                     obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1015                     obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1016         });
1017
1018         return result;
1019     }
1020
1021     private Data mapToState(String json) {
1022         JsonObject responseObject = parseString(json).getAsJsonObject();
1023
1024         JsonObject data = responseObject.getAsJsonObject("data");
1025
1026         @Nullable
1027         String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1028
1029         return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1030     }
1031
1032     private Request createRequest(HttpMethod method, String path)
1033             throws AuthorizationException, CommunicationException {
1034         return client.newRequest(apiUrl + path)
1035                 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1036                 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1037     }
1038
1039     private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1040             @Nullable ContentResponse response, @Nullable String responseBody) {
1041         HomeConnectRequest homeConnectRequest = map(request, requestBody);
1042         @Nullable
1043         HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1044
1045         logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1046         trackApiRequest(homeConnectRequest, homeConnectResponse);
1047     }
1048
1049     private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1050             @Nullable HomeConnectResponse homeConnectResponse) {
1051         if (logger.isDebugEnabled()) {
1052             StringBuilder sb = new StringBuilder();
1053
1054             if (haId != null) {
1055                 sb.append("[").append(haId).append("] ");
1056             }
1057
1058             sb.append(homeConnectRequest.getMethod()).append(" ");
1059             if (homeConnectResponse != null) {
1060                 sb.append(homeConnectResponse.getCode()).append(" ");
1061             }
1062             sb.append(homeConnectRequest.getUrl()).append("\n");
1063             homeConnectRequest.getHeader()
1064                     .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1065
1066             if (homeConnectRequest.getBody() != null) {
1067                 sb.append(homeConnectRequest.getBody()).append("\n");
1068             }
1069
1070             if (homeConnectResponse != null) {
1071                 sb.append("\n");
1072                 homeConnectResponse.getHeader()
1073                         .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1074             }
1075             if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1076                 sb.append(homeConnectResponse.getBody()).append("\n");
1077             }
1078
1079             logger.debug("{}", sb.toString());
1080         }
1081     }
1082
1083     private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1084             @Nullable HomeConnectResponse homeConnectResponse) {
1085         communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1086     }
1087
1088     private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1089         Map<String, String> headers = new HashMap<>();
1090         request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1091
1092         return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1093                 requestBody != null ? formatJsonBody(requestBody) : null);
1094     }
1095
1096     private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1097         Map<String, String> headers = new HashMap<>();
1098         response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1099
1100         return new HomeConnectResponse(response.getStatus(), headers,
1101                 responseBody != null ? formatJsonBody(responseBody) : null);
1102     }
1103 }