2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mybmw.internal.handler;
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.mybmw.internal.VehicleConfiguration;
25 import org.openhab.binding.mybmw.internal.dto.charge.ChargeSessionsContainer;
26 import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
27 import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
28 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
29 import org.openhab.binding.mybmw.internal.utils.Constants;
30 import org.openhab.binding.mybmw.internal.utils.Converter;
31 import org.openhab.binding.mybmw.internal.utils.ImageProperties;
32 import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
33 import org.openhab.core.io.net.http.HttpUtil;
34 import org.openhab.core.library.types.RawType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BridgeHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
45 import com.google.gson.JsonSyntaxException;
48 * The {@link VehicleHandler} handles responses from BMW API
50 * @author Bernd Weymann - Initial contribution
51 * @author Norbert Truchsess - edit & send charge profile
54 public class VehicleHandler extends VehicleChannelHandler {
55 private Optional<MyBMWProxy> proxy = Optional.empty();
56 private Optional<RemoteServiceHandler> remote = Optional.empty();
57 public Optional<VehicleConfiguration> configuration = Optional.empty();
58 private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
59 private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
61 private ImageProperties imageProperties = new ImageProperties();
62 VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
63 ChargeStatisticsCallback chargeStatisticsCallback = new ChargeStatisticsCallback();
64 ChargeSessionsCallback chargeSessionCallback = new ChargeSessionsCallback();
65 ByteResponseCallback imageCallback = new ImageCallback();
67 public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, String driveTrain) {
68 super(thing, cop, driveTrain);
72 public void handleCommand(ChannelUID channelUID, Command command) {
73 String group = channelUID.getGroupId();
75 // Refresh of Channels with cached values
76 if (command instanceof RefreshType) {
77 if (CHANNEL_GROUP_STATUS.equals(group)) {
78 vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
79 } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
80 imageCache.ifPresent(image -> imageCallback.onResponse(image));
82 // Check for Channel Group and corresponding Actions
83 } else if (CHANNEL_GROUP_REMOTE.equals(group)) {
84 // Executing Remote Services
85 if (command instanceof StringType) {
86 String serviceCommand = ((StringType) command).toFullString();
87 remote.ifPresent(remot -> {
88 switch (serviceCommand) {
89 case REMOTE_SERVICE_LIGHT_FLASH:
90 case REMOTE_SERVICE_DOOR_LOCK:
91 case REMOTE_SERVICE_DOOR_UNLOCK:
92 case REMOTE_SERVICE_HORN:
93 case REMOTE_SERVICE_VEHICLE_FINDER:
94 RemoteServiceUtils.getRemoteService(serviceCommand)
95 .ifPresentOrElse(service -> remot.execute(service), () -> {
96 logger.debug("Remote service execution {} unknown", serviceCommand);
99 case REMOTE_SERVICE_AIR_CONDITIONING_START:
100 RemoteServiceUtils.getRemoteService(serviceCommand)
101 .ifPresentOrElse(service -> remot.execute(service), () -> {
102 logger.debug("Remote service execution {} unknown", serviceCommand);
105 case REMOTE_SERVICE_AIR_CONDITIONING_STOP:
106 RemoteServiceUtils.getRemoteService(serviceCommand)
107 .ifPresentOrElse(service -> remot.execute(service), () -> {
108 logger.debug("Remote service execution {} unknown", serviceCommand);
112 logger.debug("Remote service execution {} unknown", serviceCommand);
117 } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
119 configuration.ifPresent(config -> {
120 if (command instanceof StringType) {
121 if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
122 String newViewport = command.toString();
123 synchronized (imageProperties) {
124 if (!imageProperties.viewport.equals(newViewport)) {
125 imageProperties = new ImageProperties(newViewport);
126 imageCache = Optional.empty();
127 proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
130 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
134 } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
135 if (command instanceof StringType) {
136 int index = Converter.getIndex(command.toFullString());
138 selectService(index);
140 logger.debug("Cannot select Service index {}", command.toFullString());
143 } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
144 if (command instanceof StringType) {
145 int index = Converter.getIndex(command.toFullString());
147 selectCheckControl(index);
149 logger.debug("Cannot select CheckControl index {}", command.toFullString());
152 } else if (CHANNEL_GROUP_CHARGE_SESSION.equals(group)) {
153 if (command instanceof StringType) {
154 int index = Converter.getIndex(command.toFullString());
156 selectSession(index);
158 logger.debug("Cannot select Session index {}", command.toFullString());
165 public void initialize() {
166 updateStatus(ThingStatus.UNKNOWN);
167 final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class);
168 configuration = Optional.of(config);
169 Bridge bridge = getBridge();
170 if (bridge != null) {
171 BridgeHandler handler = bridge.getHandler();
172 if (handler != null) {
173 proxy = ((MyBMWBridgeHandler) handler).getProxy();
174 remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
176 logger.debug("Bridge Handler null");
179 logger.debug("Bridge null");
182 imageProperties = new ImageProperties();
183 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(imageProperties.viewport));
185 // start update schedule
186 startSchedule(config.refreshInterval);
189 private void startSchedule(int interval) {
190 refreshJob.ifPresentOrElse(job -> {
191 if (job.isCancelled()) {
192 refreshJob = Optional
193 .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
194 } // else - scheduler is already running!
196 refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
201 public void dispose() {
202 refreshJob.ifPresent(job -> job.cancel(true));
203 editTimeout.ifPresent(job -> job.cancel(true));
204 remote.ifPresent(RemoteServiceHandler::cancel);
207 public void getData() {
208 proxy.ifPresentOrElse(prox -> {
209 configuration.ifPresentOrElse(config -> {
210 prox.requestVehicles(config.vehicleBrand, vehicleStatusCallback);
212 prox.requestChargeStatistics(config, chargeStatisticsCallback);
213 prox.requestChargeSessions(config, chargeSessionCallback);
215 if (!imageCache.isPresent() && !imageProperties.failLimitReached()) {
216 prox.requestImage(config, imageProperties, imageCallback);
219 logger.warn("MyBMW Vehicle Configuration isn't present");
222 logger.warn("MyBMWProxy isn't present");
226 public void updateRemoteExecutionStatus(@Nullable String service, String status) {
227 updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE,
228 StringType.valueOf((service == null ? "-" : service) + Constants.SPACE + status.toLowerCase()));
231 public Optional<VehicleConfiguration> getConfiguration() {
232 return configuration;
235 public ScheduledExecutorService getScheduler() {
239 public class ImageCallback implements ByteResponseCallback {
241 public void onResponse(byte[] content) {
242 if (content.length > 0) {
243 imageCache = Optional.of(content);
244 String contentType = HttpUtil.guessContentTypeFromData(content);
245 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType));
247 synchronized (imageProperties) {
248 imageProperties.failed();
254 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
257 public void onError(NetworkError error) {
258 logger.debug("{}", error.toString());
259 synchronized (imageProperties) {
260 imageProperties.failed();
266 * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
268 public class VehicleStatusCallback implements StringResponseCallback {
270 public void onResponse(@Nullable String content) {
271 if (content != null) {
272 if (getConfiguration().isPresent()) {
273 Vehicle v = Converter.getVehicle(configuration.get().vin, content);
275 vehicleStatusCache = Optional.of(content);
276 updateStatus(ThingStatus.ONLINE);
277 updateChannel(CHANNEL_GROUP_STATUS, RAW,
278 StringType.valueOf(Converter.getRawVehicleContent(configuration.get().vin, content)));
281 updateChargeProfile(v.status.chargingProfile);
284 logger.debug("Vehicle {} not valid", configuration.get().vin);
287 logger.debug("configuration not present");
290 updateChannel(CHANNEL_GROUP_STATUS, RAW, StringType.valueOf(Constants.EMPTY_JSON));
291 logger.debug("Content not valid");
296 public void onError(NetworkError error) {
297 logger.debug("{}", error.toString());
298 vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
299 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
303 public class ChargeStatisticsCallback implements StringResponseCallback {
305 public void onResponse(@Nullable String content) {
306 if (content != null) {
308 ChargeStatisticsContainer csc = Converter.getGson().fromJson(content,
309 ChargeStatisticsContainer.class);
311 updateChargeStatistics(csc);
313 } catch (JsonSyntaxException jse) {
314 logger.warn("{}", jse.getLocalizedMessage());
317 logger.debug("Content not valid");
322 public void onError(NetworkError error) {
323 logger.debug("{}", error.toString());
327 public class ChargeSessionsCallback implements StringResponseCallback {
329 public void onResponse(@Nullable String content) {
330 if (content != null) {
332 ChargeSessionsContainer csc = Converter.getGson().fromJson(content, ChargeSessionsContainer.class);
334 if (csc.chargingSessions != null) {
335 updateSessions(csc.chargingSessions.sessions);
338 } catch (JsonSyntaxException jse) {
339 logger.warn("{}", jse.getLocalizedMessage());
342 logger.debug("Content not valid");
347 public void onError(NetworkError error) {
348 logger.debug("{}", error.toString());