]> git.basschouten.com Git - openhab-addons.git/blob
0b1e4473d0c3020fa083e963ed1f0993e97788c0
[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 Program} or null if there is no active program
552      * @throws CommunicationException API communication exception
553      * @throws AuthorizationException oAuth authorization exception
554      * @throws ApplianceOfflineException appliance is not connected to the cloud
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 Program} or null if there is no selected program
566      * @throws CommunicationException API communication exception
567      * @throws AuthorizationException oAuth authorization exception
568      * @throws ApplianceOfflineException appliance is not connected to the cloud
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     /**
631      * Get the available options of a program.
632      *
633      * @param haId home appliance id
634      * @param programKey program id
635      * @return list of {@link AvailableProgramOption} or null if the program is unsupported by the API
636      * @throws CommunicationException API communication exception
637      * @throws AuthorizationException oAuth authorization exception
638      * @throws ApplianceOfflineException appliance is not connected to the cloud
639      */
640     public @Nullable List<AvailableProgramOption> getProgramOptions(String haId, String programKey)
641             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
642         Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey);
643         try {
644             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
645             checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
646
647             String responseBody = response.getContentAsString();
648             trackAndLogApiRequest(haId, request, null, response, responseBody);
649
650             // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram"
651             if (response.getStatus() == HttpStatus.NOT_FOUND_404
652                     && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) {
653                 throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(),
654                         responseBody == null ? "" : responseBody);
655             }
656
657             return response.getStatus() == HttpStatus.OK_200 ? mapToAvailableProgramOption(responseBody, haId) : null;
658         } catch (InterruptedException | TimeoutException | ExecutionException e) {
659             logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey,
660                     e.getMessage());
661             trackAndLogApiRequest(haId, request, null, null, null);
662             throw new CommunicationException(e);
663         }
664     }
665
666     /**
667      * Get latest API requests.
668      *
669      * @return communication queue
670      */
671     public Collection<ApiRequest> getLatestApiRequests() {
672         return communicationQueue.getAll();
673     }
674
675     private Data getSetting(String haId, String setting)
676             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
677         return getData(haId, BASE_PATH + haId + "/settings/" + setting);
678     }
679
680     private void putSettings(String haId, Data data)
681             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
682         putSettings(haId, data, VALUE_TYPE_STRING);
683     }
684
685     private void putSettings(String haId, Data data, int valueType)
686             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
687         putData(haId, BASE_PATH + haId + "/settings/" + data.getName(), data, valueType);
688     }
689
690     private Data getStatus(String haId, String status)
691             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
692         return getData(haId, BASE_PATH + haId + "/status/" + status);
693     }
694
695     public @Nullable String getRaw(String haId, String path)
696             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
697         return getRaw(haId, path, false);
698     }
699
700     public @Nullable String getRaw(String haId, String path, boolean ignoreResponseCode)
701             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
702         Request request = createRequest(HttpMethod.GET, path);
703         try {
704             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
705             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
706
707             String responseBody = response.getContentAsString();
708             trackAndLogApiRequest(haId, request, null, response, responseBody);
709
710             if (ignoreResponseCode || response.getStatus() == HttpStatus.OK_200) {
711                 return responseBody;
712             }
713         } catch (InterruptedException | TimeoutException | ExecutionException e) {
714             logger.warn("Failed to get raw! haId={}, path={}, error={}", haId, path, e.getMessage());
715             trackAndLogApiRequest(haId, request, null, null, null);
716             throw new CommunicationException(e);
717         }
718         return null;
719     }
720
721     public String putRaw(String haId, String path, String requestBodyPayload)
722             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
723         Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
724                 BSH_JSON_V1);
725         try {
726             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
727             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
728
729             String responseBody = response.getContentAsString();
730             trackAndLogApiRequest(haId, request, requestBodyPayload, response, responseBody);
731             return responseBody;
732         } catch (InterruptedException | TimeoutException | ExecutionException e) {
733             logger.warn("Failed to put raw! haId={}, path={}, payload={}, error={}", haId, path, requestBodyPayload,
734                     e.getMessage());
735             trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
736             throw new CommunicationException(e);
737         }
738     }
739
740     private @Nullable Program getProgram(String haId, String path)
741             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
742         Request request = createRequest(HttpMethod.GET, path);
743         try {
744             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
745             checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null);
746
747             String responseBody = response.getContentAsString();
748             trackAndLogApiRequest(haId, request, null, response, responseBody);
749
750             if (response.getStatus() == HttpStatus.OK_200) {
751                 return mapToProgram(responseBody);
752             }
753         } catch (InterruptedException | TimeoutException | ExecutionException e) {
754             logger.warn("Failed to get program! haId={}, path={}, error={}", haId, path, e.getMessage());
755             trackAndLogApiRequest(haId, request, null, null, null);
756             throw new CommunicationException(e);
757         }
758         return null;
759     }
760
761     private List<AvailableProgram> getAvailablePrograms(String haId, String path)
762             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
763         Request request = createRequest(HttpMethod.GET, path);
764         try {
765             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
766             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
767
768             String responseBody = response.getContentAsString();
769             trackAndLogApiRequest(haId, request, null, response, responseBody);
770
771             return mapToAvailablePrograms(responseBody, haId);
772         } catch (InterruptedException | TimeoutException | ExecutionException e) {
773             logger.warn("Failed to get available programs! haId={}, path={}, error={}", haId, path, e.getMessage());
774             trackAndLogApiRequest(haId, request, null, null, null);
775             throw new CommunicationException(e);
776         }
777     }
778
779     private void sendDelete(String haId, String path)
780             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
781         Request request = createRequest(HttpMethod.DELETE, path);
782         try {
783             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
784             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, null);
785
786             trackAndLogApiRequest(haId, request, null, response, response.getContentAsString());
787         } catch (InterruptedException | TimeoutException | ExecutionException e) {
788             logger.warn("Failed to send delete! haId={}, path={}, error={}", haId, path, e.getMessage());
789             trackAndLogApiRequest(haId, request, null, null, null);
790             throw new CommunicationException(e);
791         }
792     }
793
794     private Data getData(String haId, String path)
795             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
796         Request request = createRequest(HttpMethod.GET, path);
797         try {
798             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
799             checkResponseCode(HttpStatus.OK_200, request, response, haId, null);
800
801             String responseBody = response.getContentAsString();
802             trackAndLogApiRequest(haId, request, null, response, responseBody);
803
804             return mapToState(responseBody);
805         } catch (InterruptedException | TimeoutException | ExecutionException e) {
806             logger.warn("Failed to get data! haId={}, path={}, error={}", haId, path, e.getMessage());
807             trackAndLogApiRequest(haId, request, null, null, null);
808             throw new CommunicationException(e);
809         }
810     }
811
812     private void putData(String haId, String path, Data data, int valueType)
813             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
814         JsonObject innerObject = new JsonObject();
815         innerObject.addProperty("key", data.getName());
816
817         if (data.getValue() != null) {
818             if (valueType == VALUE_TYPE_INT) {
819                 innerObject.addProperty("value", data.getValueAsInt());
820             } else if (valueType == VALUE_TYPE_BOOLEAN) {
821                 innerObject.addProperty("value", data.getValueAsBoolean());
822             } else {
823                 innerObject.addProperty("value", data.getValue());
824             }
825         }
826
827         if (data.getUnit() != null) {
828             innerObject.addProperty("unit", data.getUnit());
829         }
830
831         JsonObject dataObject = new JsonObject();
832         dataObject.add("data", innerObject);
833         String requestBodyPayload = dataObject.toString();
834
835         Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
836                 BSH_JSON_V1);
837         try {
838             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
839             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
840
841             trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
842         } catch (InterruptedException | TimeoutException | ExecutionException e) {
843             logger.warn("Failed to put data! haId={}, path={}, data={}, valueType={}, error={}", haId, path, data,
844                     valueType, e.getMessage());
845             trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
846             throw new CommunicationException(e);
847         }
848     }
849
850     private void putOption(String haId, String path, Option option, boolean asInt)
851             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
852         JsonObject innerObject = new JsonObject();
853         innerObject.addProperty("key", option.getKey());
854
855         if (option.getValue() != null) {
856             if (asInt) {
857                 innerObject.addProperty("value", option.getValueAsInt());
858             } else {
859                 innerObject.addProperty("value", option.getValue());
860             }
861         }
862
863         if (option.getUnit() != null) {
864             innerObject.addProperty("unit", option.getUnit());
865         }
866
867         JsonArray optionsArray = new JsonArray();
868         optionsArray.add(innerObject);
869
870         JsonObject optionsObject = new JsonObject();
871         optionsObject.add("options", optionsArray);
872
873         JsonObject dataObject = new JsonObject();
874         dataObject.add("data", optionsObject);
875
876         String requestBodyPayload = dataObject.toString();
877
878         Request request = createRequest(HttpMethod.PUT, path).content(new StringContentProvider(requestBodyPayload),
879                 BSH_JSON_V1);
880         try {
881             ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId());
882             checkResponseCode(HttpStatus.NO_CONTENT_204, request, response, haId, requestBodyPayload);
883
884             trackAndLogApiRequest(haId, request, requestBodyPayload, response, response.getContentAsString());
885         } catch (InterruptedException | TimeoutException | ExecutionException e) {
886             logger.warn("Failed to put option! haId={}, path={}, option={}, asInt={}, error={}", haId, path, option,
887                     asInt, e.getMessage());
888             trackAndLogApiRequest(haId, request, requestBodyPayload, null, null);
889             throw new CommunicationException(e);
890         }
891     }
892
893     private void checkResponseCode(int desiredCode, Request request, ContentResponse response, @Nullable String haId,
894             @Nullable String requestPayload)
895             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
896         checkResponseCode(singletonList(desiredCode), request, response, haId, requestPayload);
897     }
898
899     private void checkResponseCode(List<Integer> desiredCodes, Request request, ContentResponse response,
900             @Nullable String haId, @Nullable String requestPayload)
901             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
902         if (!desiredCodes.contains(HttpStatus.UNAUTHORIZED_401)
903                 && response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
904             logger.debug("Current access token is invalid.");
905             String responseBody = response.getContentAsString();
906             trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
907             throw new AuthorizationException("Token invalid!");
908         }
909
910         if (!desiredCodes.contains(response.getStatus())) {
911             int code = response.getStatus();
912             String message = response.getReason();
913
914             logger.debug("Invalid HTTP response code {} (allowed: {})", code, desiredCodes);
915             String responseBody = response.getContentAsString();
916             trackAndLogApiRequest(haId, request, requestPayload, response, responseBody);
917
918             responseBody = responseBody == null ? "" : responseBody;
919             if (code == HttpStatus.CONFLICT_409 && responseBody.toLowerCase().contains("error")
920                     && responseBody.toLowerCase().contains("offline")) {
921                 throw new ApplianceOfflineException(code, message, responseBody);
922             } else {
923                 throw new CommunicationException(code, message, responseBody);
924             }
925         }
926     }
927
928     private Program mapToProgram(String json) {
929         ArrayList<Option> optionList = new ArrayList<>();
930         JsonObject responseObject = parseString(json).getAsJsonObject();
931         JsonObject data = responseObject.getAsJsonObject("data");
932         Program result = new Program(data.get("key").getAsString(), optionList);
933         JsonArray options = data.getAsJsonArray("options");
934
935         options.forEach(option -> {
936             JsonObject obj = (JsonObject) option;
937
938             @Nullable
939             String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
940             @Nullable
941             String value = obj.get("value") != null && !obj.get("value").isJsonNull() ? obj.get("value").getAsString()
942                     : null;
943             @Nullable
944             String unit = obj.get("unit") != null ? obj.get("unit").getAsString() : null;
945
946             optionList.add(new Option(key, value, unit));
947         });
948
949         return result;
950     }
951
952     private List<AvailableProgram> mapToAvailablePrograms(String json, String haId) {
953         ArrayList<AvailableProgram> result = new ArrayList<>();
954
955         try {
956             JsonObject responseObject = parseString(json).getAsJsonObject();
957
958             JsonArray programs = responseObject.getAsJsonObject("data").getAsJsonArray("programs");
959             programs.forEach(program -> {
960                 JsonObject obj = (JsonObject) program;
961                 @Nullable
962                 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
963                 JsonObject constraints = obj.getAsJsonObject("constraints");
964                 boolean available = constraints.get("available") != null && constraints.get("available").getAsBoolean();
965                 @Nullable
966                 String execution = constraints.get("execution") != null ? constraints.get("execution").getAsString()
967                         : null;
968
969                 if (key != null && execution != null) {
970                     result.add(new AvailableProgram(key, available, execution));
971                 }
972             });
973         } catch (Exception e) {
974             logger.warn("Could not parse available programs response! haId={}, error={}", haId, e.getMessage());
975         }
976
977         return result;
978     }
979
980     private List<AvailableProgramOption> mapToAvailableProgramOption(String json, String haId) {
981         ArrayList<AvailableProgramOption> result = new ArrayList<>();
982
983         try {
984             JsonObject responseObject = parseString(json).getAsJsonObject();
985
986             JsonArray options = responseObject.getAsJsonObject("data").getAsJsonArray("options");
987             options.forEach(option -> {
988                 JsonObject obj = (JsonObject) option;
989                 @Nullable
990                 String key = obj.get("key") != null ? obj.get("key").getAsString() : null;
991                 ArrayList<String> allowedValues = new ArrayList<>();
992                 obj.getAsJsonObject("constraints").getAsJsonArray("allowedvalues")
993                         .forEach(value -> allowedValues.add(value.getAsString()));
994
995                 if (key != null) {
996                     result.add(new AvailableProgramOption(key, allowedValues));
997                 }
998             });
999         } catch (Exception e) {
1000             logger.warn("Could not parse available program options response! haId={}, error={}", haId, e.getMessage());
1001         }
1002
1003         return result;
1004     }
1005
1006     private HomeAppliance mapToHomeAppliance(String json) {
1007         JsonObject responseObject = parseString(json).getAsJsonObject();
1008
1009         JsonObject data = responseObject.getAsJsonObject("data");
1010
1011         return new HomeAppliance(data.get("haId").getAsString(), data.get("name").getAsString(),
1012                 data.get("brand").getAsString(), data.get("vib").getAsString(), data.get("connected").getAsBoolean(),
1013                 data.get("type").getAsString(), data.get("enumber").getAsString());
1014     }
1015
1016     private ArrayList<HomeAppliance> mapToHomeAppliances(String json) {
1017         final ArrayList<HomeAppliance> result = new ArrayList<>();
1018         JsonObject responseObject = parseString(json).getAsJsonObject();
1019
1020         JsonObject data = responseObject.getAsJsonObject("data");
1021         JsonArray homeappliances = data.getAsJsonArray("homeappliances");
1022
1023         homeappliances.forEach(appliance -> {
1024             JsonObject obj = (JsonObject) appliance;
1025
1026             result.add(new HomeAppliance(obj.get("haId").getAsString(), obj.get("name").getAsString(),
1027                     obj.get("brand").getAsString(), obj.get("vib").getAsString(), obj.get("connected").getAsBoolean(),
1028                     obj.get("type").getAsString(), obj.get("enumber").getAsString()));
1029         });
1030
1031         return result;
1032     }
1033
1034     private Data mapToState(String json) {
1035         JsonObject responseObject = parseString(json).getAsJsonObject();
1036
1037         JsonObject data = responseObject.getAsJsonObject("data");
1038
1039         @Nullable
1040         String unit = data.get("unit") != null ? data.get("unit").getAsString() : null;
1041
1042         return new Data(data.get("key").getAsString(), data.get("value").getAsString(), unit);
1043     }
1044
1045     private Request createRequest(HttpMethod method, String path)
1046             throws AuthorizationException, CommunicationException {
1047         return client.newRequest(apiUrl + path)
1048                 .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(oAuthClientService))
1049                 .header(HttpHeaders.ACCEPT, BSH_JSON_V1).method(method).timeout(REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
1050     }
1051
1052     private void trackAndLogApiRequest(@Nullable String haId, Request request, @Nullable String requestBody,
1053             @Nullable ContentResponse response, @Nullable String responseBody) {
1054         HomeConnectRequest homeConnectRequest = map(request, requestBody);
1055         @Nullable
1056         HomeConnectResponse homeConnectResponse = response != null ? map(response, responseBody) : null;
1057
1058         logApiRequest(haId, homeConnectRequest, homeConnectResponse);
1059         trackApiRequest(homeConnectRequest, homeConnectResponse);
1060     }
1061
1062     private void logApiRequest(@Nullable String haId, HomeConnectRequest homeConnectRequest,
1063             @Nullable HomeConnectResponse homeConnectResponse) {
1064         if (logger.isDebugEnabled()) {
1065             StringBuilder sb = new StringBuilder();
1066
1067             if (haId != null) {
1068                 sb.append("[").append(haId).append("] ");
1069             }
1070
1071             sb.append(homeConnectRequest.getMethod()).append(" ");
1072             if (homeConnectResponse != null) {
1073                 sb.append(homeConnectResponse.getCode()).append(" ");
1074             }
1075             sb.append(homeConnectRequest.getUrl()).append("\n");
1076             homeConnectRequest.getHeader()
1077                     .forEach((key, value) -> sb.append("> ").append(key).append(": ").append(value).append("\n"));
1078
1079             if (homeConnectRequest.getBody() != null) {
1080                 sb.append(homeConnectRequest.getBody()).append("\n");
1081             }
1082
1083             if (homeConnectResponse != null) {
1084                 sb.append("\n");
1085                 homeConnectResponse.getHeader()
1086                         .forEach((key, value) -> sb.append("< ").append(key).append(": ").append(value).append("\n"));
1087             }
1088             if (homeConnectResponse != null && homeConnectResponse.getBody() != null) {
1089                 sb.append(homeConnectResponse.getBody()).append("\n");
1090             }
1091
1092             logger.debug("{}", sb.toString());
1093         }
1094     }
1095
1096     private void trackApiRequest(HomeConnectRequest homeConnectRequest,
1097             @Nullable HomeConnectResponse homeConnectResponse) {
1098         communicationQueue.add(new ApiRequest(ZonedDateTime.now(), homeConnectRequest, homeConnectResponse));
1099     }
1100
1101     private HomeConnectRequest map(Request request, @Nullable String requestBody) {
1102         Map<String, String> headers = new HashMap<>();
1103         request.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1104
1105         return new HomeConnectRequest(request.getURI().toString(), request.getMethod(), headers,
1106                 requestBody != null ? formatJsonBody(requestBody) : null);
1107     }
1108
1109     private HomeConnectResponse map(ContentResponse response, @Nullable String responseBody) {
1110         Map<String, String> headers = new HashMap<>();
1111         response.getHeaders().forEach(field -> headers.put(field.getName(), field.getValue()));
1112
1113         return new HomeConnectResponse(response.getStatus(), headers,
1114                 responseBody != null ? formatJsonBody(responseBody) : null);
1115     }
1116 }