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.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Optional;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
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;
65 import com.google.gson.JsonSyntaxException;
68 * The {@link VehicleHandler} is responsible for handling commands, which are
69 * sent to one of the channels.
71 * @author Bernd Weymann - Initial contribution
72 * @author Norbert Truchsess - edit & send charge profile
75 public class VehicleHandler extends VehicleChannelHandler {
76 private int legacyMode = Constants.INT_UNDEF; // switch to legacy API in case of 404 Errors
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();
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();
96 private Optional<ChargeProfileWrapper> chargeProfileEdit = Optional.empty();
97 private Optional<String> chargeProfileSent = Optional.empty();
99 public VehicleHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
100 super(thing, op, type, imperial);
104 public void handleCommand(ChannelUID channelUID, Command command) {
105 String group = channelUID.getGroupId();
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));
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);
142 case REMOTE_SERVICE_CHARGING_CONTROL:
143 sendChargeProfile(chargeProfileEdit);
146 logger.debug("Remote service execution {} unknown", serviceCommand);
151 } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
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));
164 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
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));
179 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType(newImageSize));
183 } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
184 if (command instanceof StringType) {
185 int index = Converter.getIndex(command.toFullString());
187 selectDestination(index);
189 logger.debug("Cannot select Destination index {}", command.toFullString());
192 } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
193 if (command instanceof StringType) {
194 int index = Converter.getIndex(command.toFullString());
196 selectService(index);
198 logger.debug("Cannot select Service index {}", command.toFullString());
201 } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
202 if (command instanceof StringType) {
203 int index = Converter.getIndex(command.toFullString());
205 selectCheckControl(index);
207 logger.debug("Cannot select CheckControl index {}", command.toFullString());
210 } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
211 handleChargeProfileCommand(channelUID, command);
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));
229 logger.debug("Bridge Handler null");
232 logger.debug("Bridge null");
235 // get Image after init with config values
236 synchronized (imageProperties) {
237 imageProperties = new ImageProperties(config.imageViewport, config.imageSize);
239 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf((config.imageViewport)));
240 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType((config.imageSize)));
242 // check imperial setting is different to AutoDetect
243 if (!UNITS_AUTODETECT.equals(config.units)) {
244 imperial = UNITS_IMPERIAL.equals(config.units);
247 // start update schedule
248 startSchedule(config.refreshInterval);
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!
258 refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
263 public void dispose() {
264 refreshJob.ifPresent(job -> job.cancel(true));
265 editTimeout.ifPresent(job -> job.cancel(true));
266 remote.ifPresent(RemoteServiceHandler::cancel);
269 public void getData() {
270 proxy.ifPresentOrElse(prox -> {
271 configuration.ifPresentOrElse(config -> {
272 if (legacyMode == 1) {
273 prox.requestLegacyVehcileStatus(config, oldVehicleStatusCallback);
275 prox.requestVehcileStatus(config, vehicleStatusCallback);
277 addCallback(vehicleStatusCallback);
278 if (isSupported(Constants.STATISTICS)) {
279 prox.requestLastTrip(config, lastTripCallback);
280 prox.requestAllTrips(config, allTripsCallback);
281 addCallback(lastTripCallback);
282 addCallback(allTripsCallback);
284 if (isSupported(Constants.LAST_DESTINATIONS)) {
285 prox.requestDestinations(config, destinationCallback);
286 addCallback(destinationCallback);
289 prox.requestChargingProfile(config, chargeProfileCallback);
290 addCallback(chargeProfileCallback);
292 synchronized (imageProperties) {
293 if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
294 prox.requestImage(config, imageProperties, imageCallback);
295 addCallback(imageCallback);
299 logger.warn("ConnectedDrive Configuration isn't present");
302 logger.warn("ConnectedDrive Proxy isn't present");
306 private synchronized void addCallback(ResponseCallback rc) {
307 callbackCounter.ifPresent(counter -> counter.add(rc));
310 private synchronized void removeCallback(ResponseCallback rc) {
311 callbackCounter.ifPresent(counter -> {
313 // all necessary callbacks received => print and set to empty
314 if (counter.isEmpty()) {
316 callbackCounter = Optional.empty();
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());
328 vehicleStatusCache.ifPresentOrElse(vehicleStatus -> {
329 logger.debug("### Vehicle Status ###");
331 // Anonymous data for VIN and Position
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;
346 logger.debug("{}", Converter.getGson().toJson(container));
347 } catch (JsonSyntaxException jse) {
348 logger.debug("{}", jse.getMessage());
351 logger.debug("### Vehicle Status Empty ###");
353 lastTripCache.ifPresentOrElse(lastTrip -> {
354 logger.debug("### Last Trip ###");
355 logger.debug("{}", lastTrip.replaceAll(vin, Constants.ANONYMOUS));
357 logger.debug("### Last Trip Empty ###");
359 allTripsCache.ifPresentOrElse(allTrips -> {
360 logger.debug("### All Trips ###");
361 logger.debug("{}", allTrips.replaceAll(vin, Constants.ANONYMOUS));
363 logger.debug("### All Trips Empty ###");
366 chargeProfileCache.ifPresentOrElse(chargeProfile -> {
367 logger.debug("### Charge Profile ###");
368 logger.debug("{}", chargeProfile.replaceAll(vin, Constants.ANONYMOUS));
370 logger.debug("### Charge Profile Empty ###");
373 destinationCache.ifPresentOrElse(destination -> {
374 logger.debug("### Charge Profile ###");
376 DestinationContainer container = Converter.getGson().fromJson(destination, DestinationContainer.class);
377 if (container != null) {
378 if (container.destinations != null) {
379 container.destinations.forEach(entry -> {
382 entry.city = Constants.ANONYMOUS;
383 entry.street = Constants.ANONYMOUS;
384 entry.streetNumber = Constants.ANONYMOUS;
385 entry.country = Constants.ANONYMOUS;
387 logger.debug("{}", Converter.getGson().toJson(container));
390 logger.debug("### Destinations Empty ###");
392 } catch (JsonSyntaxException jse) {
393 logger.debug("{}", jse.getMessage());
396 logger.debug("### Charge Profile Empty ###");
398 rangeMapCache.ifPresentOrElse(rangeMap -> {
399 logger.debug("### Range Map ###");
400 logger.debug("{}", rangeMap.replaceAll(vin, Constants.ANONYMOUS));
402 logger.debug("### Range Map Empty ###");
404 logger.debug("###### Vehicle Troubleshoot Fingerprint Data - END ######");
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
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)) {
420 // if cache is empty give it a try one time to collected Troubleshoot data
421 return lastTripCache.isEmpty() || allTripsCache.isEmpty() || destinationCache.isEmpty();
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();
429 updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE, StringType
430 .valueOf(Converter.toTitleCase((service == null ? "-" : service) + Constants.SPACE + status)));
433 public Optional<VehicleConfiguration> getConfiguration() {
434 return configuration;
437 public ScheduledExecutorService getScheduler() {
442 * Callbacks for ConnectedDrive Portal
444 * @author Bernd Weymann
447 public class ChargeProfilesCallback implements StringResponseCallback {
449 public void onResponse(@Nullable String content) {
450 if (content != null) {
451 chargeProfileCache = Optional.of(content);
452 if (chargeProfileEdit.isEmpty()) {
453 updateChargeProfileFromContent(content);
456 removeCallback(this);
460 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
463 public void onError(NetworkError error) {
464 logger.debug("{}", error.toString());
465 chargeProfileCache = Optional.of(Converter.getGson().toJson(error));
466 removeCallback(this);
470 public class RangeMapCallback implements StringResponseCallback {
472 public void onResponse(@Nullable String content) {
473 rangeMapCache = Optional.ofNullable(content);
474 removeCallback(this);
478 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
481 public void onError(NetworkError error) {
482 logger.debug("{}", error.toString());
483 rangeMapCache = Optional.of(Converter.getGson().toJson(error));
484 removeCallback(this);
488 public class DestinationsCallback implements StringResponseCallback {
491 public void onResponse(@Nullable String content) {
492 destinationCache = Optional.ofNullable(content);
493 if (content != null) {
495 DestinationContainer dc = Converter.getGson().fromJson(content, DestinationContainer.class);
496 if (dc != null && dc.destinations != null) {
497 updateDestinations(dc.destinations);
499 } catch (JsonSyntaxException jse) {
500 logger.debug("{}", jse.getMessage());
503 removeCallback(this);
507 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
510 public void onError(NetworkError error) {
511 logger.debug("{}", error.toString());
512 destinationCache = Optional.of(Converter.getGson().toJson(error));
513 removeCallback(this);
517 public class ImageCallback implements ByteResponseCallback {
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));
525 synchronized (imageProperties) {
526 imageProperties.failed();
529 removeCallback(this);
533 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
536 public void onError(NetworkError error) {
537 logger.debug("{}", error.toString());
538 synchronized (imageProperties) {
539 imageProperties.failed();
541 removeCallback(this);
545 public class AllTripsCallback implements StringResponseCallback {
547 public void onResponse(@Nullable String content) {
548 if (content != null) {
549 allTripsCache = Optional.of(content);
551 AllTripsContainer atc = Converter.getGson().fromJson(content, AllTripsContainer.class);
553 AllTrips at = atc.allTrips;
558 } catch (JsonSyntaxException jse) {
559 logger.debug("{}", jse.getMessage());
562 removeCallback(this);
566 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
569 public void onError(NetworkError error) {
570 logger.debug("{}", error.toString());
571 allTripsCache = Optional.of(Converter.getGson().toJson(error));
572 removeCallback(this);
576 public class LastTripCallback implements StringResponseCallback {
578 public void onResponse(@Nullable String content) {
579 if (content != null) {
580 lastTripCache = Optional.of(content);
582 LastTripContainer lt = Converter.getGson().fromJson(content, LastTripContainer.class);
584 LastTrip trip = lt.lastTrip;
586 updateLastTrip(trip);
589 } catch (JsonSyntaxException jse) {
590 logger.debug("{}", jse.getMessage());
593 removeCallback(this);
597 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
600 public void onError(NetworkError error) {
601 logger.debug("{}", error.toString());
602 lastTripCache = Optional.of(Converter.getGson().toJson(error));
603 removeCallback(this);
608 * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
610 public class VehicleStatusCallback implements StringResponseCallback {
612 public void onResponse(@Nullable String content) {
613 if (content != null) {
614 // switch to non legacy mode
616 updateStatus(ThingStatus.ONLINE);
617 vehicleStatusCache = Optional.of(content);
619 VehicleStatusContainer status = Converter.getGson().fromJson(content, VehicleStatusContainer.class);
620 if (status != null) {
621 VehicleStatus vStatus = status.vehicleStatus;
622 if (vStatus == null) {
625 updateVehicleStatus(vStatus);
626 updateCheckControls(vStatus.checkControlMessages);
627 updateServices(vStatus.cbsData);
628 updatePosition(vStatus.position);
630 } catch (JsonSyntaxException jse) {
631 logger.debug("{}", jse.getMessage());
634 removeCallback(this);
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);
645 vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
646 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
647 removeCallback(this);
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
656 * Selection of API was discussed here
657 * https://community.openhab.org/t/bmw-connecteddrive-bmw-i3/103876
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
662 public class LegacyVehicleStatusCallback implements StringResponseCallback {
664 public void onResponse(@Nullable String content) {
665 if (content != null) {
667 VehicleAttributesContainer vac = Converter.getGson().fromJson(content,
668 VehicleAttributesContainer.class);
669 vehicleStatusCallback.onResponse(Converter.transformLegacyStatus(vac));
671 logger.debug("VehicleStatus switched to legacy mode");
672 } catch (JsonSyntaxException jse) {
673 logger.debug("{}", jse.getMessage());
679 public void onError(NetworkError error) {
680 logger.debug("{}", error.toString());
681 vehicleStatusCallback.onError(error);
685 private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
686 if (chargeProfileEdit.isEmpty()) {
687 chargeProfileEdit = getChargeProfileWrapper();
690 chargeProfileEdit.ifPresent(profile -> {
692 boolean processed = false;
694 final String id = channelUID.getIdWithoutGroup();
696 if (command instanceof StringType) {
697 final String stringCommand = ((StringType) command).toFullString();
699 case CHARGE_PROFILE_PREFERENCE:
700 profile.setPreference(stringCommand);
701 updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
702 StringType.valueOf(Converter.toTitleCase(profile.getPreference())));
705 case CHARGE_PROFILE_MODE:
706 profile.setMode(stringCommand);
707 updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
708 StringType.valueOf(Converter.toTitleCase(profile.getMode())));
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);
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);
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);
733 profile.setTime(key, dtt.getZonedDateTime().toLocalTime());
734 updateTimedState(profile, key);
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));
749 logger.debug("unexpected command {} not processed", command.toFullString());
754 private void saveChargeProfileSent() {
755 editTimeout.ifPresent(timeout -> {
756 timeout.cancel(true);
757 editTimeout = Optional.empty();
759 chargeProfileSent.ifPresent(sent -> {
760 chargeProfileCache = Optional.of(sent);
761 chargeProfileSent = Optional.empty();
762 chargeProfileEdit = Optional.empty();
763 chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
768 public Collection<Class<? extends ThingHandlerService>> getServices() {
769 return Set.of(BMWConnectedDriveActions.class);
772 public Optional<ChargeProfileWrapper> getChargeProfileWrapper() {
773 return chargeProfileCache.flatMap(cache -> {
774 return ChargeProfileWrapper.fromJson(cache).map(wrapper -> {
777 logger.debug("cannot parse charging profile: {}", cache);
778 return Optional.empty();
781 logger.debug("No ChargeProfile recieved so far - cannot start editing");
782 return Optional.empty();
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));