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.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;
68 import com.google.gson.JsonSyntaxException;
71 * The {@link VehicleHandler} is responsible for handling commands, which are
72 * sent to one of the channels.
74 * @author Bernd Weymann - Initial contribution
75 * @author Norbert Truchsess - edit & send charge profile
78 public class VehicleHandler extends VehicleChannelHandler {
79 private int legacyMode = Constants.INT_UNDEF; // switch to legacy API in case of 404 Errors
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();
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();
100 private Optional<ChargeProfileWrapper> chargeProfileEdit = Optional.empty();
101 private Optional<String> chargeProfileSent = Optional.empty();
103 public VehicleHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
104 super(thing, op, type, imperial);
108 public void handleCommand(ChannelUID channelUID, Command command) {
109 String group = channelUID.getGroupId();
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));
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);
146 case REMOTE_SERVICE_CHARGING_CONTROL:
147 sendChargeProfile(chargeProfileEdit);
150 logger.debug("Remote service execution {} unknown", serviceCommand);
155 } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
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));
168 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
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));
183 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType(newImageSize));
187 } else if (CHANNEL_GROUP_DESTINATION.equals(group)) {
188 if (command instanceof StringType) {
189 int index = Converter.getIndex(command.toFullString());
191 selectDestination(index);
193 logger.debug("Cannot select Destination index {}", command.toFullString());
196 } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
197 if (command instanceof StringType) {
198 int index = Converter.getIndex(command.toFullString());
200 selectService(index);
202 logger.debug("Cannot select Service index {}", command.toFullString());
205 } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
206 if (command instanceof StringType) {
207 int index = Converter.getIndex(command.toFullString());
209 selectCheckControl(index);
211 logger.debug("Cannot select CheckControl index {}", command.toFullString());
214 } else if (CHANNEL_GROUP_CHARGE.equals(group)) {
215 handleChargeProfileCommand(channelUID, command);
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));
233 logger.debug("Bridge Handler null");
236 logger.debug("Bridge null");
239 // get Image after init with config values
240 synchronized (imageProperties) {
241 imageProperties = new ImageProperties(config.imageViewport, config.imageSize);
243 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf((config.imageViewport)));
244 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_SIZE, new DecimalType((config.imageSize)));
246 // check imperial setting is different to AutoDetect
247 if (!UNITS_AUTODETECT.equals(config.units)) {
248 imperial = UNITS_IMPERIAL.equals(config.units);
251 // start update schedule
252 startSchedule(config.refreshInterval);
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!
262 refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
267 public void dispose() {
268 refreshJob.ifPresent(job -> job.cancel(true));
269 editTimeout.ifPresent(job -> job.cancel(true));
270 remote.ifPresent(RemoteServiceHandler::cancel);
273 public void getData() {
274 proxy.ifPresentOrElse(prox -> {
275 configuration.ifPresentOrElse(config -> {
276 if (legacyMode == 1) {
277 prox.requestLegacyVehcileStatus(config, oldVehicleStatusCallback);
279 prox.requestVehcileStatus(config, vehicleStatusCallback);
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);
290 if (isSupported(Constants.LAST_DESTINATIONS)) {
291 prox.requestDestinations(config, destinationCallback);
292 addCallback(destinationCallback);
295 prox.requestChargingProfile(config, chargeProfileCallback);
296 addCallback(chargeProfileCallback);
298 synchronized (imageProperties) {
299 if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
300 prox.requestImage(config, imageProperties, imageCallback);
301 addCallback(imageCallback);
305 logger.warn("ConnectedDrive Configuration isn't present");
308 logger.warn("ConnectedDrive Proxy isn't present");
312 private synchronized void addCallback(ResponseCallback rc) {
313 callbackCounter.ifPresent(counter -> counter.add(rc));
316 private synchronized void removeCallback(ResponseCallback rc) {
317 callbackCounter.ifPresent(counter -> {
319 // all necessary callbacks received => print and set to empty
320 if (counter.isEmpty()) {
322 callbackCounter = Optional.empty();
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());
334 vehicleStatusCache.ifPresentOrElse(vehicleStatus -> {
335 logger.debug("### Vehicle Status ###");
337 // Anonymous data for VIN and Position
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;
352 logger.debug("{}", Converter.getGson().toJson(container));
353 } catch (JsonSyntaxException jse) {
354 logger.debug("{}", jse.getMessage());
357 logger.debug("### Vehicle Status Empty ###");
359 lastTripCache.ifPresentOrElse(lastTrip -> {
360 logger.debug("### Last Trip ###");
361 logger.debug("{}", lastTrip.replaceAll(vin, Constants.ANONYMOUS));
363 logger.debug("### Last Trip Empty ###");
365 allTripsCache.ifPresentOrElse(allTrips -> {
366 logger.debug("### All Trips ###");
367 logger.debug("{}", allTrips.replaceAll(vin, Constants.ANONYMOUS));
369 logger.debug("### All Trips Empty ###");
372 chargeProfileCache.ifPresentOrElse(chargeProfile -> {
373 logger.debug("### Charge Profile ###");
374 logger.debug("{}", chargeProfile.replaceAll(vin, Constants.ANONYMOUS));
376 logger.debug("### Charge Profile Empty ###");
379 destinationCache.ifPresentOrElse(destination -> {
380 logger.debug("### Charge Profile ###");
382 DestinationContainer container = Converter.getGson().fromJson(destination, DestinationContainer.class);
383 if (container != null) {
384 if (container.destinations != null) {
385 container.destinations.forEach(entry -> {
388 entry.city = Constants.ANONYMOUS;
389 entry.street = Constants.ANONYMOUS;
390 entry.streetNumber = Constants.ANONYMOUS;
391 entry.country = Constants.ANONYMOUS;
393 logger.debug("{}", Converter.getGson().toJson(container));
396 logger.debug("### Destinations Empty ###");
398 } catch (JsonSyntaxException jse) {
399 logger.debug("{}", jse.getMessage());
402 logger.debug("### Charge Profile Empty ###");
404 rangeMapCache.ifPresentOrElse(rangeMap -> {
405 logger.debug("### Range Map ###");
406 logger.debug("{}", rangeMap.replaceAll(vin, Constants.ANONYMOUS));
408 logger.debug("### Range Map Empty ###");
410 logger.debug("###### Vehicle Troubleshoot Fingerprint Data - END ######");
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
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)) {
426 // if cache is empty give it a try one time to collected Troubleshoot data
427 return lastTripCache.isEmpty() || allTripsCache.isEmpty() || destinationCache.isEmpty();
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();
435 updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE, StringType
436 .valueOf(Converter.toTitleCase((service == null ? "-" : service) + Constants.SPACE + status)));
439 public Optional<VehicleConfiguration> getConfiguration() {
440 return configuration;
443 public ScheduledExecutorService getScheduler() {
448 * Callbacks for ConnectedDrive Portal
450 * @author Bernd Weymann
453 public class ChargeProfilesCallback implements StringResponseCallback {
455 public void onResponse(@Nullable String content) {
456 if (content != null) {
457 chargeProfileCache = Optional.of(content);
458 if (chargeProfileEdit.isEmpty()) {
459 updateChargeProfileFromContent(content);
462 removeCallback(this);
466 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
469 public void onError(NetworkError error) {
470 logger.debug("{}", error.toString());
471 chargeProfileCache = Optional.of(Converter.getGson().toJson(error));
472 removeCallback(this);
476 public class RangeMapCallback implements StringResponseCallback {
478 public void onResponse(@Nullable String content) {
479 rangeMapCache = Optional.ofNullable(content);
480 removeCallback(this);
484 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
487 public void onError(NetworkError error) {
488 logger.debug("{}", error.toString());
489 rangeMapCache = Optional.of(Converter.getGson().toJson(error));
490 removeCallback(this);
494 public class DestinationsCallback implements StringResponseCallback {
497 public void onResponse(@Nullable String content) {
498 destinationCache = Optional.ofNullable(content);
499 if (content != null) {
501 DestinationContainer dc = Converter.getGson().fromJson(content, DestinationContainer.class);
502 if (dc != null && dc.destinations != null) {
503 updateDestinations(dc.destinations);
505 } catch (JsonSyntaxException jse) {
506 logger.debug("{}", jse.getMessage());
509 removeCallback(this);
513 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
516 public void onError(NetworkError error) {
517 logger.debug("{}", error.toString());
518 destinationCache = Optional.of(Converter.getGson().toJson(error));
519 removeCallback(this);
523 public class ImageCallback implements ByteResponseCallback {
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));
531 synchronized (imageProperties) {
532 imageProperties.failed();
535 removeCallback(this);
539 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
542 public void onError(NetworkError error) {
543 logger.debug("{}", error.toString());
544 synchronized (imageProperties) {
545 imageProperties.failed();
547 removeCallback(this);
551 public class AllTripsCallback implements StringResponseCallback {
553 public void onResponse(@Nullable String content) {
554 if (content != null) {
555 allTripsCache = Optional.of(content);
557 AllTripsContainer atc = Converter.getGson().fromJson(content, AllTripsContainer.class);
559 AllTrips at = atc.allTrips;
564 } catch (JsonSyntaxException jse) {
565 logger.debug("{}", jse.getMessage());
568 removeCallback(this);
572 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
575 public void onError(NetworkError error) {
576 logger.debug("{}", error.toString());
577 allTripsCache = Optional.of(Converter.getGson().toJson(error));
578 removeCallback(this);
582 public class LastTripCallback implements StringResponseCallback {
584 public void onResponse(@Nullable String content) {
585 if (content != null) {
586 lastTripCache = Optional.of(content);
588 LastTripContainer lt = Converter.getGson().fromJson(content, LastTripContainer.class);
590 LastTrip trip = lt.lastTrip;
592 updateLastTrip(trip);
595 } catch (JsonSyntaxException jse) {
596 logger.debug("{}", jse.getMessage());
599 removeCallback(this);
603 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
606 public void onError(NetworkError error) {
607 logger.debug("{}", error.toString());
608 lastTripCache = Optional.of(Converter.getGson().toJson(error));
609 removeCallback(this);
614 * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
616 public class VehicleStatusCallback implements StringResponseCallback {
618 public void onResponse(@Nullable String content) {
619 if (content != null) {
620 // switch to non legacy mode
622 updateStatus(ThingStatus.ONLINE);
623 vehicleStatusCache = Optional.of(content);
625 VehicleStatusContainer status = Converter.getGson().fromJson(content, VehicleStatusContainer.class);
626 if (status != null) {
627 VehicleStatus vStatus = status.vehicleStatus;
628 if (vStatus == null) {
631 updateVehicleStatus(vStatus);
632 updateCheckControls(vStatus.checkControlMessages);
633 updateServices(vStatus.cbsData);
634 updatePosition(vStatus.position);
636 } catch (JsonSyntaxException jse) {
637 logger.debug("{}", jse.getMessage());
640 removeCallback(this);
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);
651 vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
652 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
653 removeCallback(this);
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
662 * Selection of API was discussed here
663 * https://community.openhab.org/t/bmw-connecteddrive-bmw-i3/103876
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
668 public class LegacyVehicleStatusCallback implements StringResponseCallback {
670 public void onResponse(@Nullable String content) {
671 if (content != null) {
673 VehicleAttributesContainer vac = Converter.getGson().fromJson(content,
674 VehicleAttributesContainer.class);
675 vehicleStatusCallback.onResponse(Converter.transformLegacyStatus(vac));
677 logger.debug("VehicleStatus switched to legacy mode");
678 } catch (JsonSyntaxException jse) {
679 logger.debug("{}", jse.getMessage());
685 public void onError(NetworkError error) {
686 vehicleStatusCallback.onError(error);
690 public class NavigationStatusCallback implements StringResponseCallback {
692 public void onResponse(@Nullable String content) {
693 if (content != null) {
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());
701 removeCallback(this);
705 public void onError(NetworkError error) {
706 logger.debug("{}", error.toString());
707 removeCallback(this);
711 private void handleChargeProfileCommand(ChannelUID channelUID, Command command) {
712 if (chargeProfileEdit.isEmpty()) {
713 chargeProfileEdit = getChargeProfileWrapper();
716 chargeProfileEdit.ifPresent(profile -> {
718 boolean processed = false;
720 final String id = channelUID.getIdWithoutGroup();
722 if (command instanceof StringType) {
723 final String stringCommand = ((StringType) command).toFullString();
725 case CHARGE_PROFILE_PREFERENCE:
726 profile.setPreference(stringCommand);
727 updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
728 StringType.valueOf(Converter.toTitleCase(profile.getPreference())));
731 case CHARGE_PROFILE_MODE:
732 profile.setMode(stringCommand);
733 updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
734 StringType.valueOf(Converter.toTitleCase(profile.getMode())));
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);
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);
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);
759 profile.setTime(key, dtt.getZonedDateTime().toLocalTime());
760 updateTimedState(profile, key);
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));
775 logger.debug("unexpected command {} not processed", command.toFullString());
780 private void saveChargeProfileSent() {
781 editTimeout.ifPresent(timeout -> {
782 timeout.cancel(true);
783 editTimeout = Optional.empty();
785 chargeProfileSent.ifPresent(sent -> {
786 chargeProfileCache = Optional.of(sent);
787 chargeProfileSent = Optional.empty();
788 chargeProfileEdit = Optional.empty();
789 chargeProfileCache.ifPresent(this::updateChargeProfileFromContent);
794 public Collection<Class<? extends ThingHandlerService>> getServices() {
795 return Set.of(BMWConnectedDriveActions.class);
798 public Optional<ChargeProfileWrapper> getChargeProfileWrapper() {
799 return chargeProfileCache.flatMap(cache -> {
800 return ChargeProfileWrapper.fromJson(cache).map(wrapper -> {
803 logger.debug("cannot parse charging profile: {}", cache);
804 return Optional.empty();
807 logger.debug("No ChargeProfile recieved so far - cannot start editing");
808 return Optional.empty();
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));