]> git.basschouten.com Git - openhab-addons.git/blob
3d5a41a5610c266deee746a6fe1c4760cb07c559
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bmwconnecteddrive.internal.handler;
14
15 import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
16 import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
17
18 import java.nio.charset.StandardCharsets;
19 import java.util.Optional;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.util.MultiMap;
26 import org.eclipse.jetty.util.UrlEncoded;
27 import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
28 import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
29 import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
30 import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
31 import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
32 import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import com.google.gson.JsonSyntaxException;
37
38 /**
39  * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
40  *
41  * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
42  *
43  * @author Bernd Weymann - Initial contribution
44  * @author Norbert Truchsess - edit & send of charge profile
45  */
46 @NonNullByDefault
47 public class RemoteServiceHandler implements StringResponseCallback {
48     private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
49
50     private static final String SERVICE_TYPE = "serviceType";
51     private static final String EVENT_ID = "eventId";
52     private static final String DATA = "data";
53     // after 6 retries the state update will give up
54     private static final int GIVEUP_COUNTER = 6;
55     private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
56
57     private final ConnectedDriveProxy proxy;
58     private final VehicleHandler handler;
59     private final String legacyServiceExecutionAPI;
60     private final String legacyServiceExecutionStateAPI;
61     private final String serviceExecutionAPI;
62     private final String serviceExecutionStateAPI;
63
64     private int counter = 0;
65     private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
66     private Optional<String> serviceExecuting = Optional.empty();
67     private Optional<String> executingEventId = Optional.empty();
68     private boolean myBmwApiUsage = false;
69
70     public enum ExecutionState {
71         READY,
72         INITIATED,
73         PENDING,
74         DELIVERED,
75         EXECUTED,
76         ERROR,
77     }
78
79     public enum RemoteService {
80         LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights", "light-flash"),
81         VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder", "vehicle-finder"),
82         DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock", "door-lock"),
83         DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock", "door-unlock"),
84         HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow", "horn-blow"),
85         CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control", "air-conditioning"),
86         CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging", "charge-now"),
87         CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile", "charging-control");
88
89         private final String command;
90         private final String label;
91         private final String remoteCommand;
92
93         RemoteService(final String command, final String label, final String remoteCommand) {
94             this.command = command;
95             this.label = label;
96             this.remoteCommand = remoteCommand;
97         }
98
99         public String getCommand() {
100             return command;
101         }
102
103         public String getLabel() {
104             return label;
105         }
106
107         public String getRemoteCommand() {
108             return remoteCommand;
109         }
110     }
111
112     public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
113         handler = vehicleHandler;
114         proxy = connectedDriveProxy;
115         final VehicleConfiguration config = handler.getConfiguration().get();
116         legacyServiceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
117         legacyServiceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
118         serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
119         serviceExecutionStateAPI = proxy.remoteStatusUrl;
120     }
121
122     boolean execute(RemoteService service, String... data) {
123         synchronized (this) {
124             if (serviceExecuting.isPresent()) {
125                 logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
126                 // only one service executing
127                 return false;
128             }
129             serviceExecuting = Optional.of(service.name());
130         }
131         if (myBmwApiUsage) {
132             final MultiMap<String> dataMap = new MultiMap<String>();
133             if (data.length > 0) {
134                 dataMap.add(DATA, data[0]);
135                 proxy.post(serviceExecutionAPI + service.getRemoteCommand(), CONTENT_TYPE_JSON_ENCODED,
136                         "{CHARGING_PROFILE:" + data[0] + "}", this);
137             } else {
138                 proxy.post(serviceExecutionAPI + service.getRemoteCommand(), null, null, this);
139             }
140         } else {
141             final MultiMap<String> dataMap = new MultiMap<String>();
142             dataMap.add(SERVICE_TYPE, service.name());
143             if (data.length > 0) {
144                 dataMap.add(DATA, data[0]);
145             }
146             proxy.post(legacyServiceExecutionAPI, CONTENT_TYPE_URL_ENCODED,
147                     UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this);
148         }
149         return true;
150     }
151
152     public void getState() {
153         synchronized (this) {
154             serviceExecuting.ifPresentOrElse(service -> {
155                 if (counter >= GIVEUP_COUNTER) {
156                     logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
157                     reset();
158                     // immediately refresh data
159                     handler.getData();
160                 }
161                 counter++;
162                 if (myBmwApiUsage) {
163                     final MultiMap<String> dataMap = new MultiMap<String>();
164                     dataMap.add(EVENT_ID, executingEventId.get());
165                     final String encoded = dataMap == null || dataMap.isEmpty() ? null
166                             : UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false);
167
168                     proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null, this);
169                 } else {
170                     final MultiMap<String> dataMap = new MultiMap<String>();
171                     dataMap.add(SERVICE_TYPE, service);
172                     proxy.get(legacyServiceExecutionStateAPI, CONTENT_TYPE_URL_ENCODED,
173                             UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this);
174                 }
175             }, () -> {
176                 logger.warn("No Service executed to get state");
177             });
178             stateJob = Optional.empty();
179         }
180     }
181
182     @Override
183     public void onResponse(@Nullable String result) {
184         if (result != null) {
185             try {
186                 ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
187                 if (esc != null) {
188                     if (esc.executionStatus != null) {
189                         // handling of BMW ConnectedDrive updates
190                         String status = esc.executionStatus.status;
191                         if (status != null) {
192                             synchronized (this) {
193                                 handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
194                                 if (ExecutionState.EXECUTED.name().equals(status)) {
195                                     // refresh loop ends - update of status handled in the normal refreshInterval.
196                                     // Earlier
197                                     // update doesn't show better results!
198                                     reset();
199                                     return;
200                                 }
201                             }
202                         }
203                     } else if (esc.eventId != null) {
204                         // store event id for further MyBMW updates
205                         executingEventId = Optional.of(esc.eventId);
206                     } else if (esc.eventStatus != null) {
207                         // update status for MyBMW API
208                         synchronized (this) {
209                             handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), esc.eventStatus);
210                             if (ExecutionState.EXECUTED.name().equals(esc.eventStatus)) {
211                                 // refresh loop ends - update of status handled in the normal refreshInterval.
212                                 // Earlier
213                                 // update doesn't show better results!
214                                 reset();
215                                 return;
216                             }
217                         }
218                     }
219                 }
220             } catch (JsonSyntaxException jse) {
221                 logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
222             }
223         }
224         // schedule even if no result is present until retries exceeded
225         synchronized (this) {
226             stateJob.ifPresent(job -> {
227                 if (!job.isDone()) {
228                     job.cancel(true);
229                 }
230             });
231             stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
232         }
233     }
234
235     @Override
236     public void onError(NetworkError error) {
237         synchronized (this) {
238             handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
239                     ExecutionState.ERROR.name() + Constants.SPACE + Integer.toString(error.status));
240             reset();
241         }
242     }
243
244     private void reset() {
245         serviceExecuting = Optional.empty();
246         executingEventId = Optional.empty();
247         counter = 0;
248     }
249
250     public void cancel() {
251         synchronized (this) {
252             stateJob.ifPresent(action -> {
253                 if (!action.isDone()) {
254                     action.cancel(true);
255                 }
256                 stateJob = Optional.empty();
257             });
258         }
259     }
260
261     public void setMyBmwApiUsage(boolean b) {
262         myBmwApiUsage = b;
263     }
264 }