2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bmwconnecteddrive.internal.handler;
15 import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
16 import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*;
18 import java.nio.charset.StandardCharsets;
19 import java.util.Optional;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
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;
36 import com.google.gson.JsonSyntaxException;
39 * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
41 * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
43 * @author Bernd Weymann - Initial contribution
44 * @author Norbert Truchsess - edit & send of charge profile
47 public class RemoteServiceHandler implements StringResponseCallback {
48 private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
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
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;
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;
70 public enum ExecutionState {
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");
89 private final String command;
90 private final String label;
91 private final String remoteCommand;
93 RemoteService(final String command, final String label, final String remoteCommand) {
94 this.command = command;
96 this.remoteCommand = remoteCommand;
99 public String getCommand() {
103 public String getLabel() {
107 public String getRemoteCommand() {
108 return remoteCommand;
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;
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
129 serviceExecuting = Optional.of(service.name());
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);
138 proxy.post(serviceExecutionAPI + service.getRemoteCommand(), null, null, this);
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]);
146 proxy.post(legacyServiceExecutionAPI, CONTENT_TYPE_URL_ENCODED,
147 UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this);
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);
158 // immediately refresh data
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);
168 proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null, this);
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);
176 logger.warn("No Service executed to get state");
178 stateJob = Optional.empty();
183 public void onResponse(@Nullable String result) {
184 if (result != null) {
186 ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
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.
197 // update doesn't show better results!
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.
213 // update doesn't show better results!
220 } catch (JsonSyntaxException jse) {
221 logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
224 // schedule even if no result is present until retries exceeded
225 synchronized (this) {
226 stateJob.ifPresent(job -> {
231 stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
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));
244 private void reset() {
245 serviceExecuting = Optional.empty();
246 executingEventId = Optional.empty();
250 public void cancel() {
251 synchronized (this) {
252 stateJob.ifPresent(action -> {
253 if (!action.isDone()) {
256 stateJob = Optional.empty();
261 public void setMyBmwApiUsage(boolean b) {