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.*;
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
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;
33 import com.google.gson.JsonSyntaxException;
36 * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
38 * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py
40 * @author Bernd Weymann - Initial contribution
41 * @author Norbert Truchsess - edit & send of charge profile
44 public class RemoteServiceHandler implements StringResponseCallback {
45 private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
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
53 private final ConnectedDriveProxy proxy;
54 private final VehicleHandler handler;
55 private final String serviceExecutionAPI;
56 private final String serviceExecutionStateAPI;
58 private int counter = 0;
59 private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
60 private Optional<String> serviceExecuting = Optional.empty();
62 public enum ExecutionState {
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");
81 private final String command;
82 private final String label;
84 RemoteService(final String command, final String label) {
85 this.command = command;
89 public String getCommand() {
93 public String getLabel() {
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;
106 boolean execute(RemoteService service, String... data) {
107 synchronized (this) {
108 if (serviceExecuting.isPresent()) {
109 // only one service executing
112 serviceExecuting = Optional.of(service.name());
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]);
119 proxy.post(serviceExecutionAPI, dataMap, this);
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);
129 // immediately refresh data
133 final MultiMap<String> dataMap = new MultiMap<String>();
134 dataMap.add(SERVICE_TYPE, service);
135 proxy.get(serviceExecutionStateAPI, dataMap, this);
137 logger.warn("No Service executed to get state");
139 stateJob = Optional.empty();
144 public void onResponse(@Nullable String result) {
145 if (result != null) {
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!
160 } catch (JsonSyntaxException jse) {
161 logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
164 // schedule even if no result is present until retries exceeded
165 synchronized (this) {
166 stateJob.ifPresent(job -> {
171 stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
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));
184 private void reset() {
185 serviceExecuting = Optional.empty();
189 public void cancel() {
190 synchronized (this) {
191 stateJob.ifPresent(action -> {
192 if (!action.isDone()) {
195 stateJob = Optional.empty();