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