]> git.basschouten.com Git - openhab-addons.git/blob
b40e480acd926a140c30200bf0757ef6efb4f468
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mybmw.internal.handler;
14
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
16 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON_ENCODED;
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.mybmw.internal.VehicleConfiguration;
28 import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
29 import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer;
30 import org.openhab.binding.mybmw.internal.utils.Constants;
31 import org.openhab.binding.mybmw.internal.utils.Converter;
32 import org.openhab.binding.mybmw.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 <a href="https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py">
42  *      https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py</a>
43  *
44  * @author Bernd Weymann - Initial contribution
45  * @author Norbert Truchsess - edit and send of charge profile
46  */
47 @NonNullByDefault
48 public class RemoteServiceHandler implements StringResponseCallback {
49     private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
50
51     private static final String EVENT_ID = "eventId";
52     private static final String DATA = "data";
53     private static final int GIVEUP_COUNTER = 12; // after 12 retries the state update will give up
54     private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
55
56     private final MyBMWProxy proxy;
57     private final VehicleHandler handler;
58     private final String serviceExecutionAPI;
59     private final String serviceExecutionStateAPI;
60
61     private int counter = 0;
62     private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
63     private Optional<String> serviceExecuting = Optional.empty();
64     private Optional<String> executingEventId = Optional.empty();
65
66     public enum ExecutionState {
67         READY,
68         INITIATED,
69         PENDING,
70         DELIVERED,
71         EXECUTED,
72         ERROR,
73         TIMEOUT
74     }
75
76     public enum RemoteService {
77         LIGHT_FLASH("Flash Lights", REMOTE_SERVICE_LIGHT_FLASH, REMOTE_SERVICE_LIGHT_FLASH),
78         VEHICLE_FINDER("Vehicle Finder", REMOTE_SERVICE_VEHICLE_FINDER, REMOTE_SERVICE_VEHICLE_FINDER),
79         DOOR_LOCK("Door Lock", REMOTE_SERVICE_DOOR_LOCK, REMOTE_SERVICE_DOOR_LOCK),
80         DOOR_UNLOCK("Door Unlock", REMOTE_SERVICE_DOOR_UNLOCK, REMOTE_SERVICE_DOOR_UNLOCK),
81         HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN),
82         CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now?action=START"),
83         CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now?action=STOP");
84
85         private final String label;
86         private final String id;
87         private final String command;
88
89         RemoteService(final String label, final String id, String command) {
90             this.label = label;
91             this.id = id;
92             this.command = command;
93         }
94
95         public String getLabel() {
96             return label;
97         }
98
99         public String getId() {
100             return id;
101         }
102
103         public String getCommand() {
104             return command;
105         }
106     }
107
108     public RemoteServiceHandler(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) {
109         handler = vehicleHandler;
110         proxy = myBmwProxy;
111         final VehicleConfiguration config = handler.getConfiguration().get();
112         serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
113         serviceExecutionStateAPI = proxy.remoteStatusUrl;
114     }
115
116     boolean execute(RemoteService service, String... data) {
117         synchronized (this) {
118             if (serviceExecuting.isPresent()) {
119                 logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
120                 // only one service executing
121                 return false;
122             }
123             serviceExecuting = Optional.of(service.getId());
124         }
125         final MultiMap<String> dataMap = new MultiMap<String>();
126         if (data.length > 0) {
127             dataMap.add(DATA, data[0]);
128             proxy.post(serviceExecutionAPI + service.getCommand(), CONTENT_TYPE_JSON_ENCODED, data[0],
129                     handler.getConfiguration().get().vehicleBrand, this);
130         } else {
131             proxy.post(serviceExecutionAPI + service.getCommand(), null, null,
132                     handler.getConfiguration().get().vehicleBrand, this);
133         }
134         return true;
135     }
136
137     public void getState() {
138         synchronized (this) {
139             serviceExecuting.ifPresentOrElse(service -> {
140                 if (counter >= GIVEUP_COUNTER) {
141                     logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
142                     handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
143                             ExecutionState.TIMEOUT.name().toLowerCase());
144                     reset();
145                     // immediately refresh data
146                     handler.getData();
147                 } else {
148                     counter++;
149                     final MultiMap<String> dataMap = new MultiMap<String>();
150                     dataMap.add(EVENT_ID, executingEventId.get());
151                     final String encoded = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false);
152                     proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null,
153                             handler.getConfiguration().get().vehicleBrand, this);
154                 }
155             }, () -> {
156                 logger.warn("No Service executed to get state");
157             });
158             stateJob = Optional.empty();
159         }
160     }
161
162     @Override
163     public void onResponse(@Nullable String result) {
164         if (result != null) {
165             try {
166                 ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
167                 if (esc != null) {
168                     if (esc.eventId != null) {
169                         // service initiated - store event id for further MyBMW updates
170                         executingEventId = Optional.of(esc.eventId);
171                         handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
172                                 ExecutionState.INITIATED.name().toLowerCase());
173                     } else if (esc.eventStatus != null) {
174                         // service status updated
175                         synchronized (this) {
176                             handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
177                                     esc.eventStatus.toLowerCase());
178                             if (ExecutionState.EXECUTED.name().equalsIgnoreCase(esc.eventStatus)
179                                     || ExecutionState.ERROR.name().equalsIgnoreCase(esc.eventStatus)) {
180                                 // refresh loop ends - update of status handled in the normal refreshInterval.
181                                 // Earlier update doesn't show better results!
182                                 reset();
183                                 return;
184                             }
185                         }
186                     }
187                 }
188             } catch (JsonSyntaxException jse) {
189                 logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
190             }
191         }
192         // schedule even if no result is present until retries exceeded
193         synchronized (this) {
194             stateJob.ifPresent(job -> {
195                 if (!job.isDone()) {
196                     job.cancel(true);
197                 }
198             });
199             stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
200         }
201     }
202
203     @Override
204     public void onError(NetworkError error) {
205         synchronized (this) {
206             handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
207                     ExecutionState.ERROR.name().toLowerCase() + Constants.SPACE + Integer.toString(error.status));
208             reset();
209         }
210     }
211
212     private void reset() {
213         serviceExecuting = Optional.empty();
214         executingEventId = Optional.empty();
215         counter = 0;
216     }
217
218     public void cancel() {
219         synchronized (this) {
220             stateJob.ifPresent(action -> {
221                 if (!action.isDone()) {
222                     action.cancel(true);
223                 }
224                 stateJob = Optional.empty();
225             });
226         }
227     }
228 }