]> git.basschouten.com Git - openhab-addons.git/blob
78c104068bc2f236a1d67791a726b403ca167712
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.mybmw.internal.handler;
14
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
16
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
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;
45
46 import com.google.gson.JsonSyntaxException;
47
48 /**
49  * The {@link VehicleHandler} handles responses from BMW API
50  *
51  * @author Bernd Weymann - Initial contribution
52  * @author Norbert Truchsess - edit and send charge profile
53  */
54 @NonNullByDefault
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();
61
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();
67
68     public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String driveTrain) {
69         super(thing, cop, lp, driveTrain);
70     }
71
72     @Override
73     public void handleCommand(ChannelUID channelUID, Command command) {
74         String group = channelUID.getGroupId();
75
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));
82             }
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);
98                                     });
99                             break;
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);
104                                     });
105                             break;
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);
110                                     });
111                             break;
112                         default:
113                             logger.debug("Remote service execution {} unknown", serviceCommand);
114                             break;
115                     }
116                 });
117             }
118         } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
119             // Image Change
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));
129                             }
130                         }
131                         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport));
132                     }
133                 }
134             });
135         } else if (CHANNEL_GROUP_SERVICE.equals(group)) {
136             if (command instanceof StringType) {
137                 int index = Converter.getIndex(command.toFullString());
138                 if (index != -1) {
139                     selectService(index);
140                 } else {
141                     logger.debug("Cannot select Service index {}", command.toFullString());
142                 }
143             }
144         } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) {
145             if (command instanceof StringType) {
146                 int index = Converter.getIndex(command.toFullString());
147                 if (index != -1) {
148                     selectCheckControl(index);
149                 } else {
150                     logger.debug("Cannot select CheckControl index {}", command.toFullString());
151                 }
152             }
153         } else if (CHANNEL_GROUP_CHARGE_SESSION.equals(group)) {
154             if (command instanceof StringType) {
155                 int index = Converter.getIndex(command.toFullString());
156                 if (index != -1) {
157                     selectSession(index);
158                 } else {
159                     logger.debug("Cannot select Session index {}", command.toFullString());
160                 }
161             }
162         }
163     }
164
165     @Override
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));
176             } else {
177                 logger.debug("Bridge Handler null");
178             }
179         } else {
180             logger.debug("Bridge null");
181         }
182
183         imageProperties = new ImageProperties();
184         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(imageProperties.viewport));
185
186         // start update schedule
187         startSchedule(config.refreshInterval);
188     }
189
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!
196         }, () -> {
197             refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
198         });
199     }
200
201     @Override
202     public void dispose() {
203         refreshJob.ifPresent(job -> job.cancel(true));
204         editTimeout.ifPresent(job -> job.cancel(true));
205         remote.ifPresent(RemoteServiceHandler::cancel);
206     }
207
208     public void getData() {
209         proxy.ifPresentOrElse(prox -> {
210             configuration.ifPresentOrElse(config -> {
211                 prox.requestVehicles(config.vehicleBrand, vehicleStatusCallback);
212                 if (isElectric) {
213                     prox.requestChargeStatistics(config, chargeStatisticsCallback);
214                     prox.requestChargeSessions(config, chargeSessionCallback);
215                 }
216                 if (imageCache.isEmpty() && !imageProperties.failLimitReached()) {
217                     prox.requestImage(config, imageProperties, imageCallback);
218                 }
219             }, () -> {
220                 logger.warn("MyBMW Vehicle Configuration isn't present");
221             });
222         }, () -> {
223             logger.warn("MyBMWProxy isn't present");
224         });
225     }
226
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()));
230     }
231
232     public Optional<VehicleConfiguration> getConfiguration() {
233         return configuration;
234     }
235
236     public ScheduledExecutorService getScheduler() {
237         return scheduler;
238     }
239
240     public class ImageCallback implements ByteResponseCallback {
241         @Override
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));
247             } else {
248                 synchronized (imageProperties) {
249                     imageProperties.failed();
250                 }
251             }
252         }
253
254         /**
255          * Store Error Report in cache variable. Via Fingerprint Channel error is logged and Issue can be raised
256          */
257         @Override
258         public void onError(NetworkError error) {
259             logger.debug("{}", error.toString());
260             synchronized (imageProperties) {
261                 imageProperties.failed();
262             }
263         }
264     }
265
266     /**
267      * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status
268      */
269     public class VehicleStatusCallback implements StringResponseCallback {
270         @Override
271         public void onResponse(@Nullable String content) {
272             if (content != null) {
273                 if (getConfiguration().isPresent()) {
274                     Vehicle v = Converter.getVehicle(configuration.get().vin, content);
275                     if (v.valid) {
276                         vehicleStatusCache = Optional.of(content);
277                         updateStatus(ThingStatus.ONLINE);
278                         updateChannel(CHANNEL_GROUP_STATUS, RAW,
279                                 StringType.valueOf(Converter.getRawVehicleContent(configuration.get().vin, content)));
280                         updateVehicle(v);
281                         if (isElectric) {
282                             updateChargeProfile(v.status.chargingProfile);
283                         }
284                     } else {
285                         logger.debug("Vehicle {} not valid", configuration.get().vin);
286                     }
287                 } else {
288                     logger.debug("configuration not present");
289                 }
290             } else {
291                 updateChannel(CHANNEL_GROUP_STATUS, RAW, StringType.valueOf(Constants.EMPTY_JSON));
292                 logger.debug("Content not valid");
293             }
294         }
295
296         @Override
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);
301         }
302     }
303
304     public class ChargeStatisticsCallback implements StringResponseCallback {
305         @Override
306         public void onResponse(@Nullable String content) {
307             if (content != null) {
308                 try {
309                     ChargeStatisticsContainer csc = Converter.getGson().fromJson(content,
310                             ChargeStatisticsContainer.class);
311                     if (csc != null) {
312                         updateChargeStatistics(csc);
313                     }
314                 } catch (JsonSyntaxException jse) {
315                     logger.warn("{}", jse.getLocalizedMessage());
316                 }
317             } else {
318                 logger.debug("Content not valid");
319             }
320         }
321
322         @Override
323         public void onError(NetworkError error) {
324             logger.debug("{}", error.toString());
325         }
326     }
327
328     public class ChargeSessionsCallback implements StringResponseCallback {
329         @Override
330         public void onResponse(@Nullable String content) {
331             if (content != null) {
332                 try {
333                     ChargeSessionsContainer csc = Converter.getGson().fromJson(content, ChargeSessionsContainer.class);
334                     if (csc != null) {
335                         if (csc.chargingSessions != null) {
336                             updateSessions(csc.chargingSessions.sessions);
337                         }
338                     }
339                 } catch (JsonSyntaxException jse) {
340                     logger.warn("{}", jse.getLocalizedMessage());
341                 }
342             } else {
343                 logger.debug("Content not valid");
344             }
345         }
346
347         @Override
348         public void onError(NetworkError error) {
349             logger.debug("{}", error.toString());
350         }
351     }
352 }