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.bmwconnecteddrive.internal.handler;
15 import static org.openhab.binding.bmwconnecteddrive.internal.utils.Constants.ANONYMOUS;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.List;
20 import java.util.Optional;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration;
27 import org.openhab.binding.bmwconnecteddrive.internal.discovery.VehicleDiscovery;
28 import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError;
29 import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.Dealer;
30 import org.openhab.binding.bmwconnecteddrive.internal.dto.discovery.VehiclesContainer;
31 import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants;
32 import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
33 import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
34 import org.openhab.core.io.net.http.HttpClientFactory;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseBridgeHandler;
41 import org.openhab.core.thing.binding.ThingHandler;
42 import org.openhab.core.thing.binding.ThingHandlerService;
43 import org.openhab.core.types.Command;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.google.gson.JsonParseException;
50 * The {@link ConnectedDriveBridgeHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Bernd Weymann - Initial contribution
56 public class ConnectedDriveBridgeHandler extends BaseBridgeHandler implements StringResponseCallback {
57 private final Logger logger = LoggerFactory.getLogger(ConnectedDriveBridgeHandler.class);
58 private HttpClientFactory httpClientFactory;
59 private Optional<VehicleDiscovery> discoveryService = Optional.empty();
60 private Optional<ConnectedDriveProxy> proxy = Optional.empty();
61 private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
62 private Optional<String> troubleshootFingerprint = Optional.empty();
64 public ConnectedDriveBridgeHandler(Bridge bridge, HttpClientFactory hcf) {
66 httpClientFactory = hcf;
70 public void handleCommand(ChannelUID channelUID, Command command) {
71 // no commands available
75 public void initialize() {
76 troubleshootFingerprint = Optional.empty();
77 updateStatus(ThingStatus.UNKNOWN);
78 ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class);
79 logger.debug("Prefer MyBMW API {}", config.preferMyBmw);
80 if (!checkConfiguration(config)) {
81 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
83 proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config));
84 // give the system some time to create all predefined Vehicles
85 // check with API call if bridge is online
86 initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS));
87 Bridge b = super.getThing();
88 List<Thing> children = b.getThings();
89 logger.debug("Update {} things", children.size());
90 children.forEach(entry -> {
91 ThingHandler th = entry.getHandler();
96 logger.debug("Handler is null");
102 public static boolean checkConfiguration(ConnectedDriveConfiguration config) {
103 if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
106 return BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region);
111 public void dispose() {
112 initializerJob.ifPresent(job -> job.cancel(true));
115 public void requestVehicles() {
116 proxy.ifPresent(prox -> prox.requestVehicles(this));
119 // https://www.bmw-connecteddrive.de/api/me/vehicles/v2?all=true&brand=BM
120 public String getDiscoveryFingerprint() {
121 return troubleshootFingerprint.map(fingerprint -> {
122 VehiclesContainer container = null;
124 container = Converter.getGson().fromJson(fingerprint, VehiclesContainer.class);
125 if (container != null) {
126 if (container.vehicles != null) {
127 if (container.vehicles.isEmpty()) {
128 return Constants.EMPTY_JSON;
130 container.vehicles.forEach(entry -> {
131 entry.vin = ANONYMOUS;
132 entry.breakdownNumber = ANONYMOUS;
133 if (entry.dealer != null) {
134 Dealer d = entry.dealer;
136 d.country = ANONYMOUS;
139 d.postalCode = ANONYMOUS;
140 d.street = ANONYMOUS;
143 return Converter.getGson().toJson(container);
146 logger.debug("container.vehicles is null");
149 } catch (JsonParseException jpe) {
150 logger.debug("Cannot parse fingerprint {}", jpe.getMessage());
152 // Not a VehiclesContainer or Vehicles is empty so deliver fingerprint as it is
154 }).orElse(Constants.INVALID);
157 private void logFingerPrint() {
158 logger.debug("###### Discovery Troubleshoot Fingerprint Data - BEGIN ######");
159 logger.debug("### Discovery Result ###");
160 logger.debug("{}", getDiscoveryFingerprint());
161 logger.debug("###### Discovery Troubleshoot Fingerprint Data - END ######");
165 * There's only the Vehicles response available
168 public void onResponse(@Nullable String response) {
169 boolean firstResponse = troubleshootFingerprint.isEmpty();
170 if (response != null) {
171 updateStatus(ThingStatus.ONLINE);
172 troubleshootFingerprint = discoveryService.map(discovery -> {
174 VehiclesContainer container = Converter.getGson().fromJson(response, VehiclesContainer.class);
175 if (container != null) {
176 if (container.vehicles != null) {
177 discovery.onResponse(container);
178 container.vehicles.forEach(entry -> {
179 entry.vin = ANONYMOUS;
180 entry.breakdownNumber = ANONYMOUS;
181 if (entry.dealer != null) {
182 Dealer d = entry.dealer;
184 d.country = ANONYMOUS;
187 d.postalCode = ANONYMOUS;
188 d.street = ANONYMOUS;
193 troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
195 } catch (JsonParseException jpe) {
196 logger.debug("Fingerprint parse exception {}", jpe.getMessage());
198 // Unparseable or not a VehiclesContainer:
202 troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON);
210 public void onError(NetworkError error) {
211 boolean firstResponse = troubleshootFingerprint.isEmpty();
212 troubleshootFingerprint = Optional.of(error.toJson());
216 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason);
220 public Collection<Class<? extends ThingHandlerService>> getServices() {
221 return Collections.singleton(VehicleDiscovery.class);
224 public Optional<ConnectedDriveProxy> getProxy() {
228 public void setDiscoveryService(VehicleDiscovery discoveryService) {
229 this.discoveryService = Optional.of(discoveryService);