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