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