2 * Copyright (c) 2010-2023 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.i18n.LocationProvider;
34 import org.openhab.core.io.net.http.HttpUtil;
35 import org.openhab.core.library.types.RawType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BridgeHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
46 import com.google.gson.JsonSyntaxException;
49 * The {@link VehicleHandler} handles responses from BMW API
51 * @author Bernd Weymann - Initial contribution
52 * @author Norbert Truchsess - edit & send charge profile
55 public class VehicleHandler extends VehicleChannelHandler {
56 private Optional<MyBMWProxy> proxy = Optional.empty();
57 private Optional<RemoteServiceHandler> remote = Optional.empty();
58 public Optional<VehicleConfiguration> configuration = Optional.empty();
59 private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
60 private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
62 private ImageProperties imageProperties = new ImageProperties();
63 VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback();
64 ChargeStatisticsCallback chargeStatisticsCallback = new ChargeStatisticsCallback();
65 ChargeSessionsCallback chargeSessionCallback = new ChargeSessionsCallback();
66 ByteResponseCallback imageCallback = new ImageCallback();
68 public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String driveTrain) {
69 super(thing, cop, lp, driveTrain);
73 public void handleCommand(ChannelUID channelUID, Command command) {
74 String group = channelUID.getGroupId();
76 // Refresh of Channels with cached values
77 if (command instanceof RefreshType) {
78 if (CHANNEL_GROUP_STATUS.equals(group)) {
79 vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus));
80 } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
81 imageCache.ifPresent(image -> imageCallback.onResponse(image));
83 // Check for Channel Group and corresponding Actions
84 } else if (CHANNEL_GROUP_REMOTE.equals(group)) {
85 // Executing Remote Services
86 if (command instanceof StringType str) {
87 String serviceCommand = str.toFullString();
88 remote.ifPresent(remot -> {
89 switch (serviceCommand) {
90 case REMOTE_SERVICE_LIGHT_FLASH:
91 case REMOTE_SERVICE_DOOR_LOCK:
92 case REMOTE_SERVICE_DOOR_UNLOCK:
93 case REMOTE_SERVICE_HORN:
94 case REMOTE_SERVICE_VEHICLE_FINDER:
95 RemoteServiceUtils.getRemoteService(serviceCommand)
96 .ifPresentOrElse(service -> remot.execute(service), () -> {
97 logger.debug("Remote service execution {} unknown", serviceCommand);
100 case REMOTE_SERVICE_AIR_CONDITIONING_START:
101 RemoteServiceUtils.getRemoteService(serviceCommand)
102 .ifPresentOrElse(service -> remot.execute(service), () -> {
103 logger.debug("Remote service execution {} unknown", serviceCommand);
106 case REMOTE_SERVICE_AIR_CONDITIONING_STOP:
107 RemoteServiceUtils.getRemoteService(serviceCommand)
108 .ifPresentOrElse(service -> remot.execute(service), () -> {
109 logger.debug("Remote service execution {} unknown", serviceCommand);
113 logger.debug("Remote service execution {} unknown", serviceCommand);
118 } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
120 configuration.ifPresent(config -> {
121 if (command instanceof StringType) {
122 if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
123 String newViewport = command.toString();
124 synchronized (imageProperties) {
125 if (!imageProperties.viewport.equals(newViewport)) {
126 imageProperties = new ImageProperties(newViewport);
127 imageCache = Optional.empty();
128 proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback));
131 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
135 } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
136 if (command instanceof StringType) {
137 int index = Converter.getIndex(command.toFullString());
139 selectService(index);
141 logger.debug("Cannot select Service index {}", command.toFullString());
144 } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
145 if (command instanceof StringType) {
146 int index = Converter.getIndex(command.toFullString());
148 selectCheckControl(index);
150 logger.debug("Cannot select CheckControl index {}", command.toFullString());
153 } else if (CHANNEL_GROUP_CHARGE_SESSION.equals(group)) {
154 if (command instanceof StringType) {
155 int index = Converter.getIndex(command.toFullString());
157 selectSession(index);
159 logger.debug("Cannot select Session index {}", command.toFullString());
166 public void initialize() {
167 updateStatus(ThingStatus.UNKNOWN);
168 final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class);
169 configuration = Optional.of(config);
170 Bridge bridge = getBridge();
171 if (bridge != null) {
172 BridgeHandler handler = bridge.getHandler();
173 if (handler != null) {
174 proxy = ((MyBMWBridgeHandler) handler).getProxy();
175 remote = proxy.map(prox -> prox.getRemoteServiceHandler(this));
177 logger.debug("Bridge Handler null");
180 logger.debug("Bridge null");
183 imageProperties = new ImageProperties();
184 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(imageProperties.viewport));
186 // start update schedule
187 startSchedule(config.refreshInterval);
190 private void startSchedule(int interval) {
191 refreshJob.ifPresentOrElse(job -> {
192 if (job.isCancelled()) {
193 refreshJob = Optional
194 .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
195 } // else - scheduler is already running!
197 refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
202 public void dispose() {
203 refreshJob.ifPresent(job -> job.cancel(true));
204 editTimeout.ifPresent(job -> job.cancel(true));
205 remote.ifPresent(RemoteServiceHandler::cancel);
208 public void getData() {
209 proxy.ifPresentOrElse(prox -> {
210 configuration.ifPresentOrElse(config -> {
211 prox.requestVehicles(config.vehicleBrand, vehicleStatusCallback);
213 prox.requestChargeStatistics(config, chargeStatisticsCallback);
214 prox.requestChargeSessions(config, chargeSessionCallback);
216 if (imageCache.isEmpty() && !imageProperties.failLimitReached()) {
217 prox.requestImage(config, imageProperties, imageCallback);
220 logger.warn("MyBMW Vehicle Configuration isn't present");
223 logger.warn("MyBMWProxy isn't present");
227 public void updateRemoteExecutionStatus(@Nullable String service, String status) {
228 updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE,
229 StringType.valueOf((service == null ? "-" : service) + Constants.SPACE + status.toLowerCase()));
232 public Optional<VehicleConfiguration> getConfiguration() {
233 return configuration;
236 public ScheduledExecutorService getScheduler() {
240 public class ImageCallback implements ByteResponseCallback {
242 public void onResponse(byte[] content) {
243 if (content.length > 0) {
244 imageCache = Optional.of(content);
245 String contentType = HttpUtil.guessContentTypeFromData(content);
246 updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType));
248 synchronized (imageProperties) {
249 imageProperties.failed();
255 * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
258 public void onError(NetworkError error) {
259 logger.debug("{}", error.toString());
260 synchronized (imageProperties) {
261 imageProperties.failed();
267 * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
269 public class VehicleStatusCallback implements StringResponseCallback {
271 public void onResponse(@Nullable String content) {
272 if (content != null) {
273 if (getConfiguration().isPresent()) {
274 Vehicle v = Converter.getVehicle(configuration.get().vin, content);
276 vehicleStatusCache = Optional.of(content);
277 updateStatus(ThingStatus.ONLINE);
278 updateChannel(CHANNEL_GROUP_STATUS, RAW,
279 StringType.valueOf(Converter.getRawVehicleContent(configuration.get().vin, content)));
282 updateChargeProfile(v.status.chargingProfile);
285 logger.debug("Vehicle {} not valid", configuration.get().vin);
288 logger.debug("configuration not present");
291 updateChannel(CHANNEL_GROUP_STATUS, RAW, StringType.valueOf(Constants.EMPTY_JSON));
292 logger.debug("Content not valid");
297 public void onError(NetworkError error) {
298 logger.debug("{}", error.toString());
299 vehicleStatusCache = Optional.of(Converter.getGson().toJson(error));
300 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
304 public class ChargeStatisticsCallback implements StringResponseCallback {
306 public void onResponse(@Nullable String content) {
307 if (content != null) {
309 ChargeStatisticsContainer csc = Converter.getGson().fromJson(content,
310 ChargeStatisticsContainer.class);
312 updateChargeStatistics(csc);
314 } catch (JsonSyntaxException jse) {
315 logger.warn("{}", jse.getLocalizedMessage());
318 logger.debug("Content not valid");
323 public void onError(NetworkError error) {
324 logger.debug("{}", error.toString());
328 public class ChargeSessionsCallback implements StringResponseCallback {
330 public void onResponse(@Nullable String content) {
331 if (content != null) {
333 ChargeSessionsContainer csc = Converter.getGson().fromJson(content, ChargeSessionsContainer.class);
335 if (csc.chargingSessions != null) {
336 updateSessions(csc.chargingSessions.sessions);
339 } catch (JsonSyntaxException jse) {
340 logger.warn("{}", jse.getLocalizedMessage());
343 logger.debug("Content not valid");
348 public void onError(NetworkError error) {
349 logger.debug("{}", error.toString());