]> git.basschouten.com Git - openhab-addons.git/blob
4ea338e7b703594e6eddc88ac6eb964e6c866ced
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.Arrays.asList;
16 import static java.util.Collections.singletonList;
17 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
18 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.formatJsonBody;
19 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.getAuthorizationHeader;
20 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.parseString;
21 import static org.openhab.binding.homeconnect.internal.client.HttpHelper.sendRequest;
22
23 import java.time.ZonedDateTime;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
34
35 import javax.ws.rs.core.HttpHeaders;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.client.api.ContentResponse;
41 import org.eclipse.jetty.client.api.Request;
42 import org.eclipse.jetty.client.util.StringContentProvider;
43 import org.eclipse.jetty.http.HttpMethod;
44 import org.eclipse.jetty.http.HttpStatus;
45 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
46 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
47 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
48 import org.openhab.binding.homeconnect.internal.client.model.ApiRequest;
49 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
50 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
51 import org.openhab.binding.homeconnect.internal.client.model.Data;
52 import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance;
53 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest;
54 import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse;
55 import org.openhab.binding.homeconnect.internal.client.model.Option;
56 import org.openhab.binding.homeconnect.internal.client.model.Program;
57 import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration;
58 import org.openhab.core.auth.client.oauth2.OAuthClientService;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import com.google.gson.JsonArray;
63 import com.google.gson.JsonObject;
64
65 /**
66  * Client for Home Connect API.
67  *
68  * @author Jonas BrĂ¼stel - Initial contribution
69  * @author Laurent Garnier - Replace okhttp by the Jetty HTTP client provided by the openHAB core framework
70  *
71  */
72 @NonNullByDefault
73 public class HomeConnectApiClient {
74     private static final String BSH_JSON_V1 = "application/vnd.bsh.sdk.v1+json";
75     private static final String BASE = "/api/homeappliances";
76     private static final String BASE_PATH = BASE + "/";
77     private static final int REQUEST_TIMEOUT_SEC = 30;
78     private static final int VALUE_TYPE_STRING = 0;
79     private static final int VALUE_TYPE_INT = 1;
80     private static final int VALUE_TYPE_BOOLEAN = 2;
81     private static final int COMMUNICATION_QUEUE_SIZE = 50;
82
83     private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
84     private final HttpClient client;
85     private final String apiUrl;
86     private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
87     private final OAuthClientService oAuthClientService;
88     private final CircularQueue<ApiRequest> communicationQueue;
89     private final ApiBridgeConfiguration apiBridgeConfiguration;
90
91     public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClientService, boolean simulated,
92             @Nullable List<ApiRequest> apiRequestHistory, ApiBridgeConfiguration apiBridgeConfiguration) {
93         this.client = httpClient;
94         this.oAuthClientService = oAuthClientService;
95         this.apiBridgeConfiguration = apiBridgeConfiguration;
96
97         availableProgramOptionsCache = new ConcurrentHashMap<>();
98         apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
99         communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
100         if (apiRequestHistory != null) {
101             communicationQueue.addAll(apiRequestHistory);
102         }
103     }
104
105     /**
106      * Get all home appliances
107      *
108      * @return list of {@link HomeAppliance}
109      * @throws CommunicationException API communication exception
110      * @throws AuthorizationException oAuth authorization exception
111      */
112     public List<HomeAppliance> getHomeAppliances() throws CommunicationException, AuthorizationException {
113         Request request = createRequest(HttpMethod.GET, BASE);
114         try {
115             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
116             checkResponseCode(HttpStatus.OK_200, request, response, null, null);
117
118             String responseBody = response.getContentAsString();
119             trackAndLogApiRequest(null, request, null, response, responseBody);
120
121             return mapToHomeAppliances(responseBody);
122         } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
123             logger.warn("Failed to fetch home appliances! error={}", e.getMessage());
124             trackAndLogApiRequest(null, request, null, null, null);
125             throw new CommunicationException(e);
126         }
127     }
128
129     /**
130      * Get home appliance by id
131      *
132      * @param haId home appliance id
133      * @return {@link HomeAppliance}
134      * @throws CommunicationException API communication exception
135      * @throws AuthorizationException oAuth authorization exception
136      */
137     public HomeAppliance getHomeAppliance(String haId) throws CommunicationException, AuthorizationException {
138         Request request = createRequest(HttpMethod.GET, BASE_PATH + haId);
139         try {
140             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
141             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
142
143             String responseBody = response.getContentAsString();
144             trackAndLogApiRequest(haId, request, null, response, responseBody);
145
146             return mapToHomeAppliance(responseBody);
147         } catch (InterruptedException | TimeoutException | ExecutionException | ApplianceOfflineException e) {
148             logger.warn("Failed to get home appliance! haId={}, error={}", haId, e.getMessage());
149             trackAndLogApiRequest(haId, request, null, null, null);
150             throw new CommunicationException(e);
151         }
152     }
153
154     /**
155      * Get ambient light state of device.
156      *
157      * @param haId home appliance id
158      * @return {@link Data}
159      * @throws CommunicationException API communication exception
160      * @throws AuthorizationException oAuth authorization exception
161      * @throws ApplianceOfflineException appliance is not connected to the cloud
162      */
163     public Data getAmbientLightState(String haId)
164             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
165         return getSetting(haId, SETTING_AMBIENT_LIGHT_ENABLED);
166     }
167
168     /**
169      * Set ambient light state of device.
170      *
171      * @param haId home appliance id
172      * @param enable enable or disable ambient light
173      * @throws CommunicationException API communication exception
174      * @throws AuthorizationException oAuth authorization exception
175      * @throws ApplianceOfflineException appliance is not connected to the cloud
176      */
177     public void setAmbientLightState(String haId, boolean enable)
178             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
179         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_ENABLED, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
180     }
181
182     /**
183      * Get functional light state of device.
184      *
185      * @param haId home appliance id
186      * @return {@link Data}
187      * @throws CommunicationException API communication exception
188      * @throws AuthorizationException oAuth authorization exception
189      * @throws ApplianceOfflineException appliance is not connected to the cloud
190      */
191     public Data getFunctionalLightState(String haId)
192             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
193         return getSetting(haId, SETTING_LIGHTING);
194     }
195
196     /**
197      * Set functional light state of device.
198      *
199      * @param haId home appliance id
200      * @param enable enable or disable functional light
201      * @throws CommunicationException API communication exception
202      * @throws AuthorizationException oAuth authorization exception
203      * @throws ApplianceOfflineException appliance is not connected to the cloud
204      */
205     public void setFunctionalLightState(String haId, boolean enable)
206             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
207         putSettings(haId, new Data(SETTING_LIGHTING, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
208     }
209
210     /**
211      * Get functional light brightness state of device.
212      *
213      * @param haId home appliance id
214      * @return {@link Data}
215      * @throws CommunicationException API communication exception
216      * @throws AuthorizationException oAuth authorization exception
217      * @throws ApplianceOfflineException appliance is not connected to the cloud
218      */
219     public Data getFunctionalLightBrightnessState(String haId)
220             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
221         return getSetting(haId, SETTING_LIGHTING_BRIGHTNESS);
222     }
223
224     /**
225      * Set functional light brightness of device.
226      *
227      * @param haId home appliance id
228      * @param value brightness value 10-100
229      * @throws CommunicationException API communication exception
230      * @throws AuthorizationException oAuth authorization exception
231      * @throws ApplianceOfflineException appliance is not connected to the cloud
232      */
233     public void setFunctionalLightBrightnessState(String haId, int value)
234             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
235         putSettings(haId, new Data(SETTING_LIGHTING_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
236     }
237
238     /**
239      * Get ambient light brightness state of device.
240      *
241      * @param haId home appliance id
242      * @return {@link Data}
243      * @throws CommunicationException API communication exception
244      * @throws AuthorizationException oAuth authorization exception
245      * @throws ApplianceOfflineException appliance is not connected to the cloud
246      */
247     public Data getAmbientLightBrightnessState(String haId)
248             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
249         return getSetting(haId, SETTING_AMBIENT_LIGHT_BRIGHTNESS);
250     }
251
252     /**
253      * Set ambient light brightness of device.
254      *
255      * @param haId home appliance id
256      * @param value brightness value 10-100
257      * @throws CommunicationException API communication exception
258      * @throws AuthorizationException oAuth authorization exception
259      * @throws ApplianceOfflineException appliance is not connected to the cloud
260      */
261     public void setAmbientLightBrightnessState(String haId, int value)
262             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
263         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_BRIGHTNESS, String.valueOf(value), "%"), VALUE_TYPE_INT);
264     }
265
266     /**
267      * Get ambient light color state of device.
268      *
269      * @param haId home appliance id
270      * @return {@link Data}
271      * @throws CommunicationException API communication exception
272      * @throws AuthorizationException oAuth authorization exception
273      * @throws ApplianceOfflineException appliance is not connected to the cloud
274      */
275     public Data getAmbientLightColorState(String haId)
276             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
277         return getSetting(haId, SETTING_AMBIENT_LIGHT_COLOR);
278     }
279
280     /**
281      * Set ambient light color of device.
282      *
283      * @param haId home appliance id
284      * @param value color code
285      * @throws CommunicationException API communication exception
286      * @throws AuthorizationException oAuth authorization exception
287      * @throws ApplianceOfflineException appliance is not connected to the cloud
288      */
289     public void setAmbientLightColorState(String haId, String value)
290             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
291         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_COLOR, value, null));
292     }
293
294     /**
295      * Get ambient light custom color state of device.
296      *
297      * @param haId home appliance id
298      * @return {@link Data}
299      * @throws CommunicationException API communication exception
300      * @throws AuthorizationException oAuth authorization exception
301      * @throws ApplianceOfflineException appliance is not connected to the cloud
302      */
303     public Data getAmbientLightCustomColorState(String haId)
304             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
305         return getSetting(haId, SETTING_AMBIENT_LIGHT_CUSTOM_COLOR);
306     }
307
308     /**
309      * Set ambient light color of device.
310      *
311      * @param haId home appliance id
312      * @param value color code
313      * @throws CommunicationException API communication exception
314      * @throws AuthorizationException oAuth authorization exception
315      * @throws ApplianceOfflineException appliance is not connected to the cloud
316      */
317     public void setAmbientLightCustomColorState(String haId, String value)
318             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
319         putSettings(haId, new Data(SETTING_AMBIENT_LIGHT_CUSTOM_COLOR, value, null));
320     }
321
322     /**
323      * Get power state of device.
324      *
325      * @param haId home appliance id
326      * @return {@link Data}
327      * @throws CommunicationException API communication exception
328      * @throws AuthorizationException oAuth authorization exception
329      * @throws ApplianceOfflineException appliance is not connected to the cloud
330      */
331     public Data getPowerState(String haId)
332             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
333         return getSetting(haId, SETTING_POWER_STATE);
334     }
335
336     /**
337      * Set power state of device.
338      *
339      * @param haId home appliance id
340      * @param state target state
341      * @throws CommunicationException API communication exception
342      * @throws AuthorizationException oAuth authorization exception
343      * @throws ApplianceOfflineException appliance is not connected to the cloud
344      */
345     public void setPowerState(String haId, String state)
346             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
347         putSettings(haId, new Data(SETTING_POWER_STATE, state, null));
348     }
349
350     /**
351      * Get setpoint temperature of freezer
352      *
353      * @param haId home appliance id
354      * @return {@link Data}
355      * @throws CommunicationException API communication exception
356      * @throws AuthorizationException oAuth authorization exception
357      * @throws ApplianceOfflineException appliance is not connected to the cloud
358      */
359     public Data getFreezerSetpointTemperature(String haId)
360             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
361         return getSetting(haId, SETTING_FREEZER_SETPOINT_TEMPERATURE);
362     }
363
364     /**
365      * Set setpoint temperature of freezer
366      *
367      * @param haId home appliance id
368      * @param state new temperature
369      * @param unit temperature unit
370      * @throws CommunicationException API communication exception
371      * @throws AuthorizationException oAuth authorization exception
372      * @throws ApplianceOfflineException appliance is not connected to the cloud
373      */
374     public void setFreezerSetpointTemperature(String haId, String state, String unit)
375             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
376         putSettings(haId, new Data(SETTING_FREEZER_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
377     }
378
379     /**
380      * Get setpoint temperature of fridge
381      *
382      * @param haId home appliance id
383      * @return {@link Data} or null in case of communication error
384      * @throws CommunicationException API communication exception
385      * @throws AuthorizationException oAuth authorization exception
386      * @throws ApplianceOfflineException appliance is not connected to the cloud
387      */
388     public Data getFridgeSetpointTemperature(String haId)
389             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
390         return getSetting(haId, SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE);
391     }
392
393     /**
394      * Set setpoint temperature of fridge
395      *
396      * @param haId home appliance id
397      * @param state new temperature
398      * @param unit temperature unit
399      * @throws CommunicationException API communication exception
400      * @throws AuthorizationException oAuth authorization exception
401      * @throws ApplianceOfflineException appliance is not connected to the cloud
402      */
403     public void setFridgeSetpointTemperature(String haId, String state, String unit)
404             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
405         putSettings(haId, new Data(SETTING_REFRIGERATOR_SETPOINT_TEMPERATURE, state, unit), VALUE_TYPE_INT);
406     }
407
408     /**
409      * Get fridge super mode
410      *
411      * @param haId home appliance id
412      * @return {@link Data}
413      * @throws CommunicationException API communication exception
414      * @throws AuthorizationException oAuth authorization exception
415      * @throws ApplianceOfflineException appliance is not connected to the cloud
416      */
417     public Data getFridgeSuperMode(String haId)
418             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
419         return getSetting(haId, SETTING_REFRIGERATOR_SUPER_MODE);
420     }
421
422     /**
423      * Set fridge super mode
424      *
425      * @param haId home appliance id
426      * @param enable enable or disable fridge super mode
427      * @throws CommunicationException API communication exception
428      * @throws AuthorizationException oAuth authorization exception
429      * @throws ApplianceOfflineException appliance is not connected to the cloud
430      */
431     public void setFridgeSuperMode(String haId, boolean enable)
432             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
433         putSettings(haId, new Data(SETTING_REFRIGERATOR_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
434     }
435
436     /**
437      * Get freezer super mode
438      *
439      * @param haId home appliance id
440      * @return {@link Data}
441      * @throws CommunicationException API communication exception
442      * @throws AuthorizationException oAuth authorization exception
443      * @throws ApplianceOfflineException appliance is not connected to the cloud
444      */
445     public Data getFreezerSuperMode(String haId)
446             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
447         return getSetting(haId, SETTING_FREEZER_SUPER_MODE);
448     }
449
450     /**
451      * Set freezer super mode
452      *
453      * @param haId home appliance id
454      * @param enable enable or disable freezer super mode
455      * @throws CommunicationException API communication exception
456      * @throws AuthorizationException oAuth authorization exception
457      * @throws ApplianceOfflineException appliance is not connected to the cloud
458      */
459     public void setFreezerSuperMode(String haId, boolean enable)
460             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
461         putSettings(haId, new Data(SETTING_FREEZER_SUPER_MODE, String.valueOf(enable), null), VALUE_TYPE_BOOLEAN);
462     }
463
464     /**
465      * Get door state of device.
466      *
467      * @param haId home appliance id
468      * @return {@link Data}
469      * @throws CommunicationException API communication exception
470      * @throws AuthorizationException oAuth authorization exception
471      * @throws ApplianceOfflineException appliance is not connected to the cloud
472      */
473     public Data getDoorState(String haId)
474             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
475         return getStatus(haId, STATUS_DOOR_STATE);
476     }
477
478     /**
479      * Get operation state of device.
480      *
481      * @param haId home appliance id
482      * @return {@link Data}
483      * @throws CommunicationException API communication exception
484      * @throws AuthorizationException oAuth authorization exception
485      * @throws ApplianceOfflineException appliance is not connected to the cloud
486      */
487     public Data getOperationState(String haId)
488             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
489         return getStatus(haId, STATUS_OPERATION_STATE);
490     }
491
492     /**
493      * Get current cavity temperature of oven.
494      *
495      * @param haId home appliance id
496      * @return {@link Data}
497      * @throws CommunicationException API communication exception
498      * @throws AuthorizationException oAuth authorization exception
499      * @throws ApplianceOfflineException appliance is not connected to the cloud
500      */
501     public Data getCurrentCavityTemperature(String haId)
502             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
503         return getStatus(haId, STATUS_OVEN_CURRENT_CAVITY_TEMPERATURE);
504     }
505
506     /**
507      * Is remote start allowed?
508      *
509      * @param haId haId home appliance id
510      * @return true or false
511      * @throws CommunicationException API communication exception
512      * @throws AuthorizationException oAuth authorization exception
513      * @throws ApplianceOfflineException appliance is not connected to the cloud
514      */
515     public boolean isRemoteControlStartAllowed(String haId)
516             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
517         Data data = getStatus(haId, STATUS_REMOTE_CONTROL_START_ALLOWED);
518         return Boolean.parseBoolean(data.getValue());
519     }
520
521     /**
522      * Is remote control allowed?
523      *
524      * @param haId haId home appliance id
525      * @return true or false
526      * @throws CommunicationException API communication exception
527      * @throws AuthorizationException oAuth authorization exception
528      * @throws ApplianceOfflineException appliance is not connected to the cloud
529      */
530     public boolean isRemoteControlActive(String haId)
531             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
532         Data data = getStatus(haId, STATUS_REMOTE_CONTROL_ACTIVE);
533         return Boolean.parseBoolean(data.getValue());
534     }
535
536     /**
537      * Is local control allowed?
538      *
539      * @param haId haId home appliance id
540      * @return true or false
541      * @throws CommunicationException API communication exception
542      * @throws AuthorizationException oAuth authorization exception
543      * @throws ApplianceOfflineException appliance is not connected to the cloud
544      */
545     public boolean isLocalControlActive(String haId)
546             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
547         Data data = getStatus(haId, STATUS_LOCAL_CONTROL_ACTIVE);
548         return Boolean.parseBoolean(data.getValue());
549     }
550
551     /**
552      * Get active program of device.
553      *
554      * @param haId home appliance id
555      * @return {@link Data} or null if there is no active program
556      * @throws CommunicationException API communication exception
557      * @throws AuthorizationException oAuth authorization exception
558      * @throws ApplianceOfflineException appliance is not connected to the cloud
559      */
560     public @Nullable Program getActiveProgram(String haId)
561             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
562         return getProgram(haId, BASE_PATH + haId + "/programs/active");
563     }
564
565     /**
566      * Get selected program of device.
567      *
568      * @param haId home appliance id
569      * @return {@link Data} or null if there is no selected program
570      * @throws CommunicationException API communication exception
571      * @throws AuthorizationException oAuth authorization exception
572      * @throws ApplianceOfflineException appliance is not connected to the cloud
573      */
574     public @Nullable Program getSelectedProgram(String haId)
575             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
576         return getProgram(haId, BASE_PATH + haId + "/programs/selected");
577     }
578
579     public void setSelectedProgram(String haId, String program)
580             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
581         putData(haId, BASE_PATH + haId + "/programs/selected", new Data(program, null, null), VALUE_TYPE_STRING);
582     }
583
584     public void startProgram(String haId, String program)
585             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
586         putData(haId, BASE_PATH + haId + "/programs/active", new Data(program, null, null), VALUE_TYPE_STRING);
587     }
588
589     public void startSelectedProgram(String haId)
590             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
591         String selectedProgram = getRaw(haId, BASE_PATH + haId + "/programs/selected");
592         if (selectedProgram != null) {
593             putRaw(haId, BASE_PATH + haId + "/programs/active", selectedProgram);
594         }
595     }
596
597     public void startCustomProgram(String haId, String json)
598             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
599         putRaw(haId, BASE_PATH + haId + "/programs/active", json);
600     }
601
602     public void setProgramOptions(String haId, String key, String value, @Nullable String unit, boolean valueAsInt,
603             boolean isProgramActive) throws CommunicationException, AuthorizationException, ApplianceOfflineException {
604         String programState = isProgramActive ? "active" : "selected";
605
606         putOption(haId, BASE_PATH + haId + "/programs/" + programState + "/options", new Option(key, value, unit),
607                 valueAsInt);
608     }
609
610     public void stopProgram(String haId)
611             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
612         sendDelete(haId, BASE_PATH + haId + "/programs/active");
613     }
614
615     public List<AvailableProgram> getPrograms(String haId)
616             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
617         return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
618     }
619
620     public List<AvailableProgram> getAvailablePrograms(String haId)
621             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
622         return getAvailablePrograms(haId, BASE_PATH + haId + "/programs/available");
623     }
624
625     public List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
626             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
627         if (availableProgramOptionsCache.containsKey(programKey)) {
628             logger.debug("Returning cached options for '{}'.", programKey);
629             List<AvailableProgramOption> availableProgramOptions = availableProgramOptionsCache.get(programKey);
630             return availableProgramOptions != null ? availableProgramOptions : Collections.emptyList();
631         }
632
633         Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
634         try {
635             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
636             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
637
638             String responseBody = response.getContentAsString();
639             trackAndLogApiRequest(haId, request, null, response, responseBody);
640
641             List<AvailableProgramOption> availableProgramOptions = mapToAvailableProgramOption(responseBody, haId);
642             availableProgramOptionsCache.put(programKey, availableProgramOptions);
643             return availableProgramOptions;
644         } catch (InterruptedException | TimeoutException | ExecutionException e) {
645             logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
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(asList(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(singletonList(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 }