]> git.basschouten.com Git - openhab-addons.git/blob
fc1999d46aac066b291743dcd87170aa5db1d558
[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.bmwconnecteddrive.internal.handler;
14
15 import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
16
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.util.MultiMap;
24 import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
25 import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
26 import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer;
27 import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
28 import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
29 import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import com.google.gson.JsonSyntaxException;
34
35 /**
36  * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
37  *
38  * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
39  *
40  * @author Bernd Weymann - Initial contribution
41  * @author Norbert Truchsess - edit & send of charge profile
42  */
43 @NonNullByDefault
44 public class RemoteServiceHandler implements StringResponseCallback {
45     private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
46
47     private static final String SERVICE_TYPE = "serviceType";
48     private static final String DATA = "data";
49     // after 6 retries the state update will give up
50     private static final int GIVEUP_COUNTER = 6;
51     private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
52
53     private final ConnectedDriveProxy proxy;
54     private final VehicleHandler handler;
55     private final String serviceExecutionAPI;
56     private final String serviceExecutionStateAPI;
57
58     private int counter = 0;
59     private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
60     private Optional<String> serviceExecuting = Optional.empty();
61
62     public enum ExecutionState {
63         READY,
64         INITIATED,
65         PENDING,
66         DELIVERED,
67         EXECUTED,
68         ERROR,
69     }
70
71     public enum RemoteService {
72         LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"),
73         VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"),
74         DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"),
75         DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"),
76         HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"),
77         CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"),
78         CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"),
79         CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile");
80
81         private final String command;
82         private final String label;
83
84         RemoteService(final String command, final String label) {
85             this.command = command;
86             this.label = label;
87         }
88
89         public String getCommand() {
90             return command;
91         }
92
93         public String getLabel() {
94             return label;
95         }
96     }
97
98     public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) {
99         handler = vehicleHandler;
100         proxy = connectedDriveProxy;
101         final VehicleConfiguration config = handler.getConfiguration().get();
102         serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI;
103         serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI;
104     }
105
106     boolean execute(RemoteService service, String... data) {
107         synchronized (this) {
108             if (serviceExecuting.isPresent()) {
109                 // only one service executing
110                 return false;
111             }
112             serviceExecuting = Optional.of(service.name());
113         }
114         final MultiMap<String> dataMap = new MultiMap<String>();
115         dataMap.add(SERVICE_TYPE, service.name());
116         if (data.length > 0) {
117             dataMap.add(DATA, data[0]);
118         }
119         proxy.post(serviceExecutionAPI, dataMap, this);
120         return true;
121     }
122
123     public void getState() {
124         synchronized (this) {
125             serviceExecuting.ifPresentOrElse(service -> {
126                 if (counter >= GIVEUP_COUNTER) {
127                     logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
128                     reset();
129                     // immediately refresh data
130                     handler.getData();
131                 }
132                 counter++;
133                 final MultiMap<String> dataMap = new MultiMap<String>();
134                 dataMap.add(SERVICE_TYPE, service);
135                 proxy.get(serviceExecutionStateAPI, dataMap, this);
136             }, () -> {
137                 logger.warn("No Service executed to get state");
138             });
139             stateJob = Optional.empty();
140         }
141     }
142
143     @Override
144     public void onResponse(@Nullable String result) {
145         if (result != null) {
146             try {
147                 ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
148                 if (esc != null && esc.executionStatus != null) {
149                     String status = esc.executionStatus.status;
150                     synchronized (this) {
151                         handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status);
152                         if (ExecutionState.EXECUTED.name().equals(status)) {
153                             // refresh loop ends - update of status handled in the normal refreshInterval. Earlier
154                             // update doesn't show better results!
155                             reset();
156                             return;
157                         }
158                     }
159                 }
160             } catch (JsonSyntaxException jse) {
161                 logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
162             }
163         }
164         // schedule even if no result is present until retries exceeded
165         synchronized (this) {
166             stateJob.ifPresent(job -> {
167                 if (!job.isDone()) {
168                     job.cancel(true);
169                 }
170             });
171             stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
172         }
173     }
174
175     @Override
176     public void onError(NetworkError error) {
177         synchronized (this) {
178             handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
179                     ExecutionState.ERROR.name() + Constants.SPACE + Integer.toString(error.status));
180             reset();
181         }
182     }
183
184     private void reset() {
185         serviceExecuting = Optional.empty();
186         counter = 0;
187     }
188
189     public void cancel() {
190         synchronized (this) {
191             stateJob.ifPresent(action -> {
192                 if (!action.isDone()) {
193                     action.cancel(true);
194                 }
195                 stateJob = Optional.empty();
196             });
197         }
198     }
199 }