]> git.basschouten.com Git - openhab-addons.git/blob
d9e9cb35716439ae3b1e097b9e28baaeaa82352b
[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.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Optional;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration;
29 import org.openhab.binding.bmwconnecteddrive.internal.action.BMWConnectedDriveActions;
30 import org.openhab.binding.bmwconnecteddrive.internal.dto.DestinationContainer;
31 import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
32 import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
33 import org.openhab.binding.bmwconnecteddrive.internal.dto.navigation.NavigationContainer;
34 import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
35 import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer;
36 import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
37 import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTripContainer;
38 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
39 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
40 import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.ExecutionState;
41 import org.openhab.binding.bmwconnecteddrive.internal.handler.RemoteServiceHandler.RemoteService;
42 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
43 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.ChargeKeyDay;
44 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
45 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
46 import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
47 import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
48 import org.openhab.binding.bmwconnecteddrive.internal.utils.ImageProperties;
49 import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
50 import org.openhab.core.io.net.http.HttpUtil;
51 import org.openhab.core.library.types.DateTimeType;
52 import org.openhab.core.library.types.DecimalType;
53 import org.openhab.core.library.types.OnOffType;
54 import org.openhab.core.library.types.QuantityType;
55 import org.openhab.core.library.types.RawType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.library.unit.Units;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.ThingStatus;
62 import org.openhab.core.thing.ThingStatusDetail;
63 import org.openhab.core.thing.binding.BridgeHandler;
64 import org.openhab.core.thing.binding.ThingHandlerService;
65 import org.openhab.core.types.Command;
66 import org.openhab.core.types.RefreshType;
67
68 import com.google.gson.JsonSyntaxException;
69
70 /**
71  * The {@link VehicleHandler} is responsible for handling commands, which are
72  * sent to one of the channels.
73  *
74  * @author Bernd Weymann - Initial contribution
75  * @author Norbert Truchsess - edit & send charge profile
76  */
77 @NonNullByDefault
78 public class VehicleHandler extends VehicleChannelHandler {
79     private int legacyMode = Constants.INT_UNDEF; // switch to legacy API in case of 404 Errors
80
81     private Optional<ConnectedDriveProxy> proxy = Optional.empty();
82     private Optional<RemoteServiceHandler> remote = Optional.empty();
83     private Optional<VehicleConfiguration> configuration = Optional.empty();
84     private Optional<ConnectedDriveBridgeHandler> bridgeHandler = Optional.empty();
85     private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
86     private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
87     private Optional<List<ResponseCallback>> callbackCounter = Optional.empty();
88
89     private ImageProperties imageProperties = new ImageProperties();
90     VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
91     StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback();
92     StringResponseCallback navigationCallback = new NavigationStatusCallback();
93     StringResponseCallback lastTripCallback = new LastTripCallback();
94     StringResponseCallback allTripsCallback = new AllTripsCallback();
95     StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback();
96     StringResponseCallback rangeMapCallback = new RangeMapCallback();
97     DestinationsCallback destinationCallback = new DestinationsCallback();
98     ByteResponseCallback imageCallback = new ImageCallback();
99
100     private Optional<ChargeProfileWrapper> chargeProfileEdit = Optional.empty();
101     private Optional<String> chargeProfileSent = Optional.empty();
102
103     public VehicleHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
104         super(thing, op, type, imperial);
105     }
106
107     @Override
108     public void handleCommand(ChannelUID channelUID, Command command) {
109         String group = channelUID.getGroupId();
110
111         // Refresh of Channels with cached values
112         if (command instanceof RefreshType) {
113             if (CHANNEL_GROUP_LAST_TRIP.equals(group)) {
114                 lastTripCache.ifPresent(lastTrip -> lastTripCallback.onResponse(lastTrip));
115             } else if (CHANNEL_GROUP_LIFETIME.equals(group)) {
116                 allTripsCache.ifPresent(allTrips -> allTripsCallback.onResponse(allTrips));
117             } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
118                 destinationCache.ifPresent(destination -> destinationCallback.onResponse(destination));
119             } else if (CHANNEL_GROUP_STATUS.equals(group)) {
120                 vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
121             } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
122                 chargeProfileEdit.ifPresentOrElse(this::updateChargeProfile,
123                         () -> chargeProfileCache.ifPresent(this::updateChargeProfileFromContent));
124             } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
125                 imageCache.ifPresent(image -> imageCallback.onResponse(image));
126             }
127             // Check for Channel Group and corresponding Actions
128         } else if (CHANNEL_GROUP_REMOTE.equals(group)) {
129             // Executing Remote Services
130             if (command instanceof StringType) {
131                 String serviceCommand = ((StringType) command).toFullString();
132                 remote.ifPresent(remot -> {
133                     switch (serviceCommand) {
134                         case REMOTE_SERVICE_LIGHT_FLASH:
135                         case REMOTE_SERVICE_AIR_CONDITIONING:
136                         case REMOTE_SERVICE_DOOR_LOCK:
137                         case REMOTE_SERVICE_DOOR_UNLOCK:
138                         case REMOTE_SERVICE_HORN:
139                         case REMOTE_SERVICE_VEHICLE_FINDER:
140                         case REMOTE_SERVICE_CHARGE_NOW:
141                             RemoteServiceUtils.getRemoteService(serviceCommand)
142                                     .ifPresentOrElse(service -> remot.execute(service), () -> {
143                                         logger.debug("Remote service execution {} unknown", serviceCommand);
144                                     });
145                             break;
146                         case REMOTE_SERVICE_CHARGING_CONTROL:
147                             sendChargeProfile(chargeProfileEdit);
148                             break;
149                         default:
150                             logger.debug("Remote service execution {} unknown", serviceCommand);
151                             break;
152                     }
153                 });
154             }
155         } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
156             // Image Change
157             configuration.ifPresent(config -> {
158                 if (command instanceof StringType) {
159                     if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
160                         String newViewport = command.toString();
161                         synchronized (imageProperties) {
162                             if (!imageProperties.viewport.equals(newViewport)) {
163                                 imageProperties = new ImageProperties(newViewport, imageProperties.size);
164                                 imageCache = Optional.empty();
165                                 proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
166                             }
167                         }
168                         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
169                     }
170                 }
171                 if (command instanceof DecimalType) {
172                     if (command instanceof DecimalType) {
173                         int newImageSize = ((DecimalType) command).intValue();
174                         if (channelUID.getIdWithoutGroup().equals(IMAGE_SIZE)) {
175                             synchronized (imageProperties) {
176                                 if (imageProperties.size != newImageSize) {
177                                     imageProperties = new ImageProperties(imageProperties.viewport, newImageSize);
178                                     imageCache = Optional.empty();
179                                     proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
180                                 }
181                             }
182                         }
183                         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType(newImageSize));
184                     }
185                 }
186             });
187         } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
188             if (command instanceof StringType) {
189                 int index = Converter.getIndex(command.toFullString());
190                 if (index != -1) {
191                     selectDestination(index);
192                 } else {
193                     logger.debug("Cannot select Destination index {}", command.toFullString());
194                 }
195             }
196         } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
197             if (command instanceof StringType) {
198                 int index = Converter.getIndex(command.toFullString());
199                 if (index != -1) {
200                     selectService(index);
201                 } else {
202                     logger.debug("Cannot select Service index {}", command.toFullString());
203                 }
204             }
205         } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
206             if (command instanceof StringType) {
207                 int index = Converter.getIndex(command.toFullString());
208                 if (index != -1) {
209                     selectCheckControl(index);
210                 } else {
211                     logger.debug("Cannot select CheckControl index {}", command.toFullString());
212                 }
213             }
214         } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
215             handleChargeProfileCommand(channelUID, command);
216         }
217     }
218
219     @Override
220     public void initialize() {
221         callbackCounter = Optional.of(new ArrayList<ResponseCallback>());
222         updateStatus(ThingStatus.UNKNOWN);
223         final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class);
224         configuration = Optional.of(config);
225         Bridge bridge = getBridge();
226         if (bridge != null) {
227             BridgeHandler handler = bridge.getHandler();
228             if (handler != null) {
229                 bridgeHandler = Optional.of(((ConnectedDriveBridgeHandler) handler));
230                 proxy = ((ConnectedDriveBridgeHandler) handler).getProxy();
231                 remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
232             } else {
233                 logger.debug("Bridge Handler null");
234             }
235         } else {
236             logger.debug("Bridge null");
237         }
238
239         // get Image after init with config values
240         synchronized (imageProperties) {
241             imageProperties = new ImageProperties(config.imageViewport, config.imageSize);
242         }
243         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf((config.imageViewport)));
244         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType((config.imageSize)));
245
246         // check imperial setting is different to AutoDetect
247         if (!UNITS_AUTODETECT.equals(config.units)) {
248             imperial = UNITS_IMPERIAL.equals(config.units);
249         }
250
251         // start update schedule
252         startSchedule(config.refreshInterval);
253     }
254
255     private void startSchedule(int interval) {
256         refreshJob.ifPresentOrElse(job -> {
257             if (job.isCancelled()) {
258                 refreshJob = Optional
259                         .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
260             } // else - scheduler is already running!
261         }, () -> {
262             refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
263         });
264     }
265
266     @Override
267     public void dispose() {
268         refreshJob.ifPresent(job -> job.cancel(true));
269         editTimeout.ifPresent(job -> job.cancel(true));
270         remote.ifPresent(RemoteServiceHandler::cancel);
271     }
272
273     public void getData() {
274         proxy.ifPresentOrElse(prox -> {
275             configuration.ifPresentOrElse(config -> {
276                 if (legacyMode == 1) {
277                     prox.requestLegacyVehcileStatus(config, oldVehicleStatusCallback);
278                 } else {
279                     prox.requestVehcileStatus(config, vehicleStatusCallback);
280                 }
281                 addCallback(vehicleStatusCallback);
282                 prox.requestLNavigation(config, navigationCallback);
283                 addCallback(navigationCallback);
284                 if (isSupported(Constants.STATISTICS)) {
285                     prox.requestLastTrip(config, lastTripCallback);
286                     prox.requestAllTrips(config, allTripsCallback);
287                     addCallback(lastTripCallback);
288                     addCallback(allTripsCallback);
289                 }
290                 if (isSupported(Constants.LAST_DESTINATIONS)) {
291                     prox.requestDestinations(config, destinationCallback);
292                     addCallback(destinationCallback);
293                 }
294                 if (isElectric) {
295                     prox.requestChargingProfile(config, chargeProfileCallback);
296                     addCallback(chargeProfileCallback);
297                 }
298                 synchronized (imageProperties) {
299                     if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
300                         prox.requestImage(config, imageProperties, imageCallback);
301                         addCallback(imageCallback);
302                     }
303                 }
304             }, () -> {
305                 logger.warn("ConnectedDrive Configuration isn't present");
306             });
307         }, () -> {
308             logger.warn("ConnectedDrive Proxy isn't present");
309         });
310     }
311
312     private synchronized void addCallback(ResponseCallback rc) {
313         callbackCounter.ifPresent(counter -> counter.add(rc));
314     }
315
316     private synchronized void removeCallback(ResponseCallback rc) {
317         callbackCounter.ifPresent(counter -> {
318             counter.remove(rc);
319             // all necessary callbacks received => print and set to empty
320             if (counter.isEmpty()) {
321                 logFingerPrint();
322                 callbackCounter = Optional.empty();
323             }
324         });
325     }
326
327     private void logFingerPrint() {
328         final String vin = configuration.map(config -> config.vin).orElse("");
329         logger.debug("###### Vehicle Troubleshoot Fingerprint Data - BEGIN ######");
330         logger.debug("### Discovery Result ###");
331         bridgeHandler.ifPresent(handler -> {
332             logger.debug("{}", handler.getDiscoveryFingerprint());
333         });
334         vehicleStatusCache.ifPresentOrElse(vehicleStatus -> {
335             logger.debug("### Vehicle Status ###");
336
337             // Anonymous data for VIN and Position
338             try {
339                 VehicleStatusContainer container = Converter.getGson().fromJson(vehicleStatus,
340                         VehicleStatusContainer.class);
341                 if (container != null) {
342                     VehicleStatus status = container.vehicleStatus;
343                     if (status != null) {
344                         status.vin = Constants.ANONYMOUS;
345                         if (status.position != null) {
346                             status.position.lat = -1;
347                             status.position.lon = -1;
348                             status.position.heading = -1;
349                         }
350                     }
351                 }
352                 logger.debug("{}", Converter.getGson().toJson(container));
353             } catch (JsonSyntaxException jse) {
354                 logger.debug("{}", jse.getMessage());
355             }
356         }, () -> {
357             logger.debug("### Vehicle Status Empty ###");
358         });
359         lastTripCache.ifPresentOrElse(lastTrip -> {
360             logger.debug("### Last Trip ###");
361             logger.debug("{}", lastTrip.replaceAll(vin, Constants.ANONYMOUS));
362         }, () -> {
363             logger.debug("### Last Trip Empty ###");
364         });
365         allTripsCache.ifPresentOrElse(allTrips -> {
366             logger.debug("### All Trips ###");
367             logger.debug("{}", allTrips.replaceAll(vin, Constants.ANONYMOUS));
368         }, () -> {
369             logger.debug("### All Trips Empty ###");
370         });
371         if (isElectric) {
372             chargeProfileCache.ifPresentOrElse(chargeProfile -> {
373                 logger.debug("### Charge Profile ###");
374                 logger.debug("{}", chargeProfile.replaceAll(vin, Constants.ANONYMOUS));
375             }, () -> {
376                 logger.debug("### Charge Profile Empty ###");
377             });
378         }
379         destinationCache.ifPresentOrElse(destination -> {
380             logger.debug("### Charge Profile ###");
381             try {
382                 DestinationContainer container = Converter.getGson().fromJson(destination, DestinationContainer.class);
383                 if (container != null) {
384                     if (container.destinations != null) {
385                         container.destinations.forEach(entry -> {
386                             entry.lat = 0;
387                             entry.lon = 0;
388                             entry.city = Constants.ANONYMOUS;
389                             entry.street = Constants.ANONYMOUS;
390                             entry.streetNumber = Constants.ANONYMOUS;
391                             entry.country = Constants.ANONYMOUS;
392                         });
393                         logger.debug("{}", Converter.getGson().toJson(container));
394                     }
395                 } else {
396                     logger.debug("### Destinations Empty ###");
397                 }
398             } catch (JsonSyntaxException jse) {
399                 logger.debug("{}", jse.getMessage());
400             }
401         }, () -> {
402             logger.debug("### Charge Profile Empty ###");
403         });
404         rangeMapCache.ifPresentOrElse(rangeMap -> {
405             logger.debug("### Range Map ###");
406             logger.debug("{}", rangeMap.replaceAll(vin, Constants.ANONYMOUS));
407         }, () -> {
408             logger.debug("### Range Map Empty ###");
409         });
410         logger.debug("###### Vehicle Troubleshoot Fingerprint Data - END ######");
411     }
412
413     /**
414      * Don't stress ConnectedDrive with unnecessary requests. One call at the beginning is done to check the response.
415      * After cache has e.g. a proper error response it will be shown in the fingerprint
416      *
417      * @return
418      */
419     private boolean isSupported(String service) {
420         final String services = thing.getProperties().get(Constants.SERVICES_SUPPORTED);
421         if (services != null) {
422             if (services.contains(service)) {
423                 return true;
424             }
425         }
426         // if cache is empty give it a try one time to collected Troubleshoot data
427         return lastTripCache.isEmpty() || allTripsCache.isEmpty() || destinationCache.isEmpty();
428     }
429
430     public void updateRemoteExecutionStatus(@Nullable String service, @Nullable String status) {
431         if (RemoteService.CHARGING_CONTROL.toString().equals(service)
432                 && ExecutionState.EXECUTED.name().equals(status)) {
433             saveChargeProfileSent();
434         }
435         updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE, StringType
436                 .valueOf(Converter.toTitleCase((service == null ? "-" : service) + Constants.SPACE + status)));
437     }
438
439     public Optional<VehicleConfiguration> getConfiguration() {
440         return configuration;
441     }
442
443     public ScheduledExecutorService getScheduler() {
444         return scheduler;
445     }
446
447     /**
448      * Callbacks for ConnectedDrive Portal
449      *
450      * @author Bernd Weymann
451      *
452      */
453     public class ChargeProfilesCallback implements StringResponseCallback {
454         @Override
455         public void onResponse(@Nullable String content) {
456             if (content != null) {
457                 chargeProfileCache = Optional.of(content);
458                 if (chargeProfileEdit.isEmpty()) {
459                     updateChargeProfileFromContent(content);
460                 }
461             }
462             removeCallback(this);
463         }
464
465         /**
466          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
467          */
468         @Override
469         public void onError(NetworkError error) {
470             logger.debug("{}", error.toString());
471             chargeProfileCache = Optional.of(Converter.getGson().toJson(error));
472             removeCallback(this);
473         }
474     }
475
476     public class RangeMapCallback implements StringResponseCallback {
477         @Override
478         public void onResponse(@Nullable String content) {
479             rangeMapCache = Optional.ofNullable(content);
480             removeCallback(this);
481         }
482
483         /**
484          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
485          */
486         @Override
487         public void onError(NetworkError error) {
488             logger.debug("{}", error.toString());
489             rangeMapCache = Optional.of(Converter.getGson().toJson(error));
490             removeCallback(this);
491         }
492     }
493
494     public class DestinationsCallback implements StringResponseCallback {
495
496         @Override
497         public void onResponse(@Nullable String content) {
498             destinationCache = Optional.ofNullable(content);
499             if (content != null) {
500                 try {
501                     DestinationContainer dc = Converter.getGson().fromJson(content, DestinationContainer.class);
502                     if (dc != null && dc.destinations != null) {
503                         updateDestinations(dc.destinations);
504                     }
505                 } catch (JsonSyntaxException jse) {
506                     logger.debug("{}", jse.getMessage());
507                 }
508             }
509             removeCallback(this);
510         }
511
512         /**
513          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
514          */
515         @Override
516         public void onError(NetworkError error) {
517             logger.debug("{}", error.toString());
518             destinationCache = Optional.of(Converter.getGson().toJson(error));
519             removeCallback(this);
520         }
521     }
522
523     public class ImageCallback implements ByteResponseCallback {
524         @Override
525         public void onResponse(byte[] content) {
526             if (content.length > 0) {
527                 imageCache = Optional.of(content);
528                 String contentType = HttpUtil.guessContentTypeFromData(content);
529                 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType));
530             } else {
531                 synchronized (imageProperties) {
532                     imageProperties.failed();
533                 }
534             }
535             removeCallback(this);
536         }
537
538         /**
539          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
540          */
541         @Override
542         public void onError(NetworkError error) {
543             logger.debug("{}", error.toString());
544             synchronized (imageProperties) {
545                 imageProperties.failed();
546             }
547             removeCallback(this);
548         }
549     }
550
551     public class AllTripsCallback implements StringResponseCallback {
552         @Override
553         public void onResponse(@Nullable String content) {
554             if (content != null) {
555                 allTripsCache = Optional.of(content);
556                 try {
557                     AllTripsContainer atc = Converter.getGson().fromJson(content, AllTripsContainer.class);
558                     if (atc != null) {
559                         AllTrips at = atc.allTrips;
560                         if (at != null) {
561                             updateAllTrips(at);
562                         }
563                     }
564                 } catch (JsonSyntaxException jse) {
565                     logger.debug("{}", jse.getMessage());
566                 }
567             }
568             removeCallback(this);
569         }
570
571         /**
572          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
573          */
574         @Override
575         public void onError(NetworkError error) {
576             logger.debug("{}", error.toString());
577             allTripsCache = Optional.of(Converter.getGson().toJson(error));
578             removeCallback(this);
579         }
580     }
581
582     public class LastTripCallback implements StringResponseCallback {
583         @Override
584         public void onResponse(@Nullable String content) {
585             if (content != null) {
586                 lastTripCache = Optional.of(content);
587                 try {
588                     LastTripContainer lt = Converter.getGson().fromJson(content, LastTripContainer.class);
589                     if (lt != null) {
590                         LastTrip trip = lt.lastTrip;
591                         if (trip != null) {
592                             updateLastTrip(trip);
593                         }
594                     }
595                 } catch (JsonSyntaxException jse) {
596                     logger.debug("{}", jse.getMessage());
597                 }
598             }
599             removeCallback(this);
600         }
601
602         /**
603          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
604          */
605         @Override
606         public void onError(NetworkError error) {
607             logger.debug("{}", error.toString());
608             lastTripCache = Optional.of(Converter.getGson().toJson(error));
609             removeCallback(this);
610         }
611     }
612
613     /**
614      * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
615      */
616     public class VehicleStatusCallback implements StringResponseCallback {
617         @Override
618         public void onResponse(@Nullable String content) {
619             if (content != null) {
620                 // switch to non legacy mode
621                 legacyMode = 0;
622                 updateStatus(ThingStatus.ONLINE);
623                 vehicleStatusCache = Optional.of(content);
624                 try {
625                     VehicleStatusContainer status = Converter.getGson().fromJson(content, VehicleStatusContainer.class);
626                     if (status != null) {
627                         VehicleStatus vStatus = status.vehicleStatus;
628                         if (vStatus == null) {
629                             return;
630                         }
631                         updateVehicleStatus(vStatus);
632                         updateCheckControls(vStatus.checkControlMessages);
633                         updateServices(vStatus.cbsData);
634                         updatePosition(vStatus.position);
635                     }
636                 } catch (JsonSyntaxException jse) {
637                     logger.debug("{}", jse.getMessage());
638                 }
639             }
640             removeCallback(this);
641         }
642
643         @Override
644         public void onError(NetworkError error) {
645             logger.debug("{}", error.toString());
646             // only if legacyMode isn't set yet try legacy API
647             if (error.status != 200 && legacyMode == Constants.INT_UNDEF) {
648                 logger.debug("VehicleStatus not found - try legacy API");
649                 proxy.get().requestLegacyVehcileStatus(configuration.get(), oldVehicleStatusCallback);
650             }
651             vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
652             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
653             removeCallback(this);
654         }
655     }
656
657     /**
658      * Fallback API if origin isn't supported.
659      * This comes from the Community Discussion where a Vehicle from 2015 answered with "404"
660      * https://community.openhab.org/t/bmw-connecteddrive-binding/105124
661      *
662      * Selection of API was discussed here
663      * https://community.openhab.org/t/bmw-connecteddrive-bmw-i3/103876
664      *
665      * I figured out that only one API was working for this Vehicle. So this backward compatible Callback is introduced.
666      * The delivered data is converted into the origin dto object so no changes in previous functional code needed
667      */
668     public class LegacyVehicleStatusCallback implements StringResponseCallback {
669         @Override
670         public void onResponse(@Nullable String content) {
671             if (content != null) {
672                 try {
673                     VehicleAttributesContainer vac = Converter.getGson().fromJson(content,
674                             VehicleAttributesContainer.class);
675                     vehicleStatusCallback.onResponse(Converter.transformLegacyStatus(vac));
676                     legacyMode = 1;
677                     logger.debug("VehicleStatus switched to legacy mode");
678                 } catch (JsonSyntaxException jse) {
679                     logger.debug("{}", jse.getMessage());
680                 }
681             }
682         }
683
684         @Override
685         public void onError(NetworkError error) {
686             vehicleStatusCallback.onError(error);
687         }
688     }
689
690     public class NavigationStatusCallback implements StringResponseCallback {
691         @Override
692         public void onResponse(@Nullable String content) {
693             if (content != null) {
694                 try {
695                     NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class);
696                     updateChannel(CHANNEL_GROUP_RANGE, SOC_MAX, QuantityType.valueOf(nav.socmax, Units.KILOWATT_HOUR));
697                 } catch (JsonSyntaxException jse) {
698                     logger.debug("{}", jse.getMessage());
699                 }
700             }
701             removeCallback(this);
702         }
703
704         @Override
705         public void onError(NetworkError error) {
706             logger.debug("{}", error.toString());
707             removeCallback(this);
708         }
709     }
710
711     private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
712         if (chargeProfileEdit.isEmpty()) {
713             chargeProfileEdit = getChargeProfileWrapper();
714         }
715
716         chargeProfileEdit.ifPresent(profile -> {
717
718             boolean processed = false;
719
720             final String id = channelUID.getIdWithoutGroup();
721
722             if (command instanceof StringType) {
723                 final String stringCommand = ((StringType) command).toFullString();
724                 switch (id) {
725                     case CHARGE_PROFILE_PREFERENCE:
726                         profile.setPreference(stringCommand);
727                         updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
728                                 StringType.valueOf(Converter.toTitleCase(profile.getPreference())));
729                         processed = true;
730                         break;
731                     case CHARGE_PROFILE_MODE:
732                         profile.setMode(stringCommand);
733                         updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
734                                 StringType.valueOf(Converter.toTitleCase(profile.getMode())));
735                         processed = true;
736                         break;
737                     default:
738                         break;
739                 }
740             } else if (command instanceof OnOffType) {
741                 final ProfileKey enableKey = ChargeProfileUtils.getEnableKey(id);
742                 if (enableKey != null) {
743                     profile.setEnabled(enableKey, OnOffType.ON.equals(command));
744                     updateTimedState(profile, enableKey);
745                     processed = true;
746                 } else {
747                     final ChargeKeyDay chargeKeyDay = ChargeProfileUtils.getKeyDay(id);
748                     if (chargeKeyDay != null) {
749                         profile.setDayEnabled(chargeKeyDay.key, chargeKeyDay.day, OnOffType.ON.equals(command));
750                         updateTimedState(profile, chargeKeyDay.key);
751                         processed = true;
752                     }
753                 }
754             } else if (command instanceof DateTimeType) {
755                 DateTimeType dtt = (DateTimeType) command;
756                 logger.debug("Accept {} for ID {}", dtt.toFullString(), id);
757                 final ProfileKey key = ChargeProfileUtils.getTimeKey(id);
758                 if (key != null) {
759                     profile.setTime(key, dtt.getZonedDateTime().toLocalTime());
760                     updateTimedState(profile, key);
761                     processed = true;
762                 }
763             }
764
765             if (processed) {
766                 // cancel current timer and add another 5 mins - valid for each edit
767                 editTimeout.ifPresent(timeout -> timeout.cancel(true));
768                 // start edit timer with 5 min timeout
769                 editTimeout = Optional.of(scheduler.schedule(() -> {
770                     editTimeout = Optional.empty();
771                     chargeProfileEdit = Optional.empty();
772                     chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
773                 }, 5, TimeUnit.MINUTES));
774             } else {
775                 logger.debug("unexpected command {} not processed", command.toFullString());
776             }
777         });
778     }
779
780     private void saveChargeProfileSent() {
781         editTimeout.ifPresent(timeout -> {
782             timeout.cancel(true);
783             editTimeout = Optional.empty();
784         });
785         chargeProfileSent.ifPresent(sent -> {
786             chargeProfileCache = Optional.of(sent);
787             chargeProfileSent = Optional.empty();
788             chargeProfileEdit = Optional.empty();
789             chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
790         });
791     }
792
793     @Override
794     public Collection<Class<? extends ThingHandlerService>> getServices() {
795         return Set.of(BMWConnectedDriveActions.class);
796     }
797
798     public Optional<ChargeProfileWrapper> getChargeProfileWrapper() {
799         return chargeProfileCache.flatMap(cache -> {
800             return ChargeProfileWrapper.fromJson(cache).map(wrapper -> {
801                 return wrapper;
802             }).or(() -> {
803                 logger.debug("cannot parse charging profile: {}", cache);
804                 return Optional.empty();
805             });
806         }).or(() -> {
807             logger.debug("No ChargeProfile recieved so far - cannot start editing");
808             return Optional.empty();
809         });
810     }
811
812     public void sendChargeProfile(Optional<ChargeProfileWrapper> profile) {
813         profile.map(profil -> profil.getJson()).ifPresent(json -> {
814             logger.debug("sending charging profile: {}", json);
815             chargeProfileSent = Optional.of(json);
816             remote.ifPresent(rem -> rem.execute(RemoteService.CHARGING_CONTROL, json));
817         });
818     }
819 }