2 * Copyright (c) 2010-2023 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.mybmw.internal.handler;
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
16 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON_ENCODED;
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.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;
36 import com.google.gson.JsonSyntaxException;
39 * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle
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>
44 * @author Bernd Weymann - Initial contribution
45 * @author Norbert Truchsess - edit and send of charge profile
48 public class RemoteServiceHandler implements StringResponseCallback {
49 private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
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
56 private final MyBMWProxy proxy;
57 private final VehicleHandler handler;
58 private final String serviceExecutionAPI;
59 private final String serviceExecutionStateAPI;
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();
66 public enum ExecutionState {
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");
85 private final String label;
86 private final String id;
87 private final String command;
89 RemoteService(final String label, final String id, String command) {
92 this.command = command;
95 public String getLabel() {
99 public String getId() {
103 public String getCommand() {
108 public RemoteServiceHandler(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) {
109 handler = vehicleHandler;
111 final VehicleConfiguration config = handler.getConfiguration().get();
112 serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
113 serviceExecutionStateAPI = proxy.remoteStatusUrl;
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
123 serviceExecuting = Optional.of(service.getId());
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);
131 proxy.post(serviceExecutionAPI + service.getCommand(), null, null,
132 handler.getConfiguration().get().vehicleBrand, this);
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());
145 // immediately refresh data
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);
156 logger.warn("No Service executed to get state");
158 stateJob = Optional.empty();
163 public void onResponse(@Nullable String result) {
164 if (result != null) {
166 ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
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!
188 } catch (JsonSyntaxException jse) {
189 logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
192 // schedule even if no result is present until retries exceeded
193 synchronized (this) {
194 stateJob.ifPresent(job -> {
199 stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
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));
212 private void reset() {
213 serviceExecuting = Optional.empty();
214 executingEventId = Optional.empty();
218 public void cancel() {
219 synchronized (this) {
220 stateJob.ifPresent(action -> {
221 if (!action.isDone()) {
224 stateJob = Optional.empty();