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.utils;
15 import java.time.LocalDateTime;
16 import java.time.ZoneId;
17 import java.time.ZonedDateTime;
18 import java.time.format.DateTimeFormatter;
19 import java.time.format.DateTimeParseException;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.Optional;
24 import javax.measure.quantity.Length;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributes;
29 import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer;
30 import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleMessages;
31 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
32 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
33 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
34 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
35 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
36 import org.openhab.core.i18n.TimeZoneProvider;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.unit.ImperialUnits;
39 import org.openhab.core.types.State;
40 import org.openhab.core.types.UnDefType;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.google.gson.Gson;
47 * The {@link Converter} Conversion Helpers
49 * @author Bernd Weymann - Initial contribution
52 public class Converter {
53 public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
55 public static final DateTimeFormatter SERVICE_DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern("yyyy-MM-dd");
56 public static final DateTimeFormatter SERVICE_DATE_OUTPUT_PATTERN = DateTimeFormatter.ofPattern("MMM yyyy");
58 public static final String LOCAL_DATE_INPUT_PATTERN_STRING = "dd.MM.yyyy HH:mm";
59 public static final DateTimeFormatter LOCAL_DATE_INPUT_PATTERN = DateTimeFormatter
60 .ofPattern(LOCAL_DATE_INPUT_PATTERN_STRING);
62 public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
63 public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
65 public static final String DATE_INPUT_ZONE_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ssZ";
66 public static final DateTimeFormatter DATE_INPUT_ZONE_PATTERN = DateTimeFormatter
67 .ofPattern(DATE_INPUT_ZONE_PATTERN_STRING);
69 public static final DateTimeFormatter DATE_OUTPUT_PATTERN = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
71 private static final Gson GSON = new Gson();
72 private static final double SCALE = 10;
73 public static final double MILES_TO_KM_RATIO = 1.60934;
74 private static final String SPLIT_HYPHEN = "-";
75 private static final String SPLIT_BRACKET = "\\(";
77 public static Optional<TimeZoneProvider> timeZoneProvider = Optional.empty();
79 public static double round(double value) {
80 return Math.round(value * SCALE) / SCALE;
83 public static String getLocalDateTimeWithoutOffest(@Nullable String input) {
85 return Constants.NULL_DATE;
88 if (input.contains(Constants.PLUS)) {
89 ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_ZONE_PATTERN);
91 ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_PATTERN);
93 return ldt.format(Converter.DATE_INPUT_PATTERN);
96 public static String getLocalDateTime(@Nullable String input) {
98 return Constants.NULL_DATE;
102 if (input.contains(Constants.PLUS)) {
103 ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_ZONE_PATTERN);
106 ldt = LocalDateTime.parse(input, Converter.DATE_INPUT_PATTERN);
107 } catch (DateTimeParseException dtpe) {
108 ldt = LocalDateTime.parse(input, Converter.LOCAL_DATE_INPUT_PATTERN);
111 ZonedDateTime zdtUTC = ldt.atZone(ZoneId.of("UTC"));
113 zdtLZ = zdtUTC.withZoneSameInstant(ZoneId.systemDefault());
114 if (timeZoneProvider.isPresent()) {
115 zdtLZ = zdtUTC.withZoneSameInstant(timeZoneProvider.get().getTimeZone());
117 zdtLZ = zdtUTC.withZoneSameInstant(ZoneId.systemDefault());
119 return zdtLZ.format(Converter.DATE_INPUT_PATTERN);
122 public static void setTimeZoneProvider(TimeZoneProvider tzp) {
123 timeZoneProvider = Optional.of(tzp);
126 public static String toTitleCase(@Nullable String input) {
128 return toTitleCase(Constants.UNDEF);
130 String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
131 String converted = toTitleCase(lower, Constants.SPACE);
132 converted = toTitleCase(converted, SPLIT_HYPHEN);
133 converted = toTitleCase(converted, SPLIT_BRACKET);
138 private static String toTitleCase(String input, String splitter) {
139 String[] arr = input.split(splitter);
140 StringBuilder sb = new StringBuilder();
141 for (int i = 0; i < arr.length; i++) {
143 sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
145 sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
147 return sb.toString().trim();
150 public static String capitalizeFirst(String str) {
151 return str.substring(0, 1).toUpperCase() + str.substring(1);
154 public static Gson getGson() {
159 * Measure distance between 2 coordinates
161 * @param sourceLatitude
162 * @param sourceLongitude
163 * @param destinationLatitude
164 * @param destinationLongitude
167 public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
168 double destinationLongitude) {
169 double earthRadius = 6378.137; // Radius of earth in KM
170 double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
171 double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
172 double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
173 * Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
174 double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
175 return earthRadius * c;
179 * Easy function but there's some measures behind:
180 * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
181 * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
182 * line from Location A to B.
183 * I've taken some measurements to calculate the overhead factor based on Google Maps
184 * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
185 * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
186 * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
188 * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
190 * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
193 * @return mapping from air-line distance to "real road" distance
195 public static double guessRangeRadius(double range) {
199 public static State getMiles(QuantityType<Length> qtLength) {
200 if (qtLength.intValue() == -1) {
201 return UnDefType.UNDEF;
203 QuantityType<Length> qt = qtLength.toUnit(ImperialUnits.MILE);
207 LOGGER.debug("Cannot convert {} to miles", qt);
208 return UnDefType.UNDEF;
212 public static int getIndex(String fullString) {
215 index = Integer.parseInt(fullString);
216 } catch (NumberFormatException nfe) {
221 public static String transformLegacyStatus(@Nullable VehicleAttributesContainer vac) {
223 if (vac.attributesMap != null && vac.vehicleMessages != null) {
224 VehicleAttributes attributesMap = vac.attributesMap;
225 VehicleMessages vehicleMessages = vac.vehicleMessages;
226 // create target objects
227 VehicleStatusContainer vsc = new VehicleStatusContainer();
228 VehicleStatus vs = new VehicleStatus();
229 vsc.vehicleStatus = vs;
231 vs.mileage = attributesMap.mileage;
232 vs.doorLockState = attributesMap.doorLockState;
234 vs.doorDriverFront = attributesMap.doorDriverFront;
235 vs.doorDriverRear = attributesMap.doorDriverRear;
236 vs.doorPassengerFront = attributesMap.doorPassengerFront;
237 vs.doorPassengerRear = attributesMap.doorPassengerRear;
238 vs.hood = attributesMap.hoodState;
239 vs.trunk = attributesMap.trunkState;
241 vs.windowDriverFront = attributesMap.winDriverFront;
242 vs.windowDriverRear = attributesMap.winDriverRear;
243 vs.windowPassengerFront = attributesMap.winPassengerFront;
244 vs.windowPassengerRear = attributesMap.winPassengerRear;
245 vs.sunroof = attributesMap.sunroofState;
247 vs.remainingFuel = attributesMap.remainingFuel;
248 vs.remainingRangeElectric = attributesMap.beRemainingRangeElectricKm;
249 vs.remainingRangeElectricMls = attributesMap.beRemainingRangeElectricMile;
250 vs.remainingRangeFuel = attributesMap.beRemainingRangeFuelKm;
251 vs.remainingRangeFuelMls = attributesMap.beRemainingRangeFuelMile;
252 vs.remainingFuel = attributesMap.remainingFuel;
253 vs.chargingLevelHv = attributesMap.chargingLevelHv;
254 vs.maxRangeElectric = attributesMap.beMaxRangeElectric;
255 vs.maxRangeElectricMls = attributesMap.beMaxRangeElectricMile;
256 vs.chargingStatus = attributesMap.chargingHVStatus;
257 vs.connectionStatus = attributesMap.connectorStatus;
258 vs.lastChargingEndReason = attributesMap.lastChargingEndReason;
260 vs.updateTime = attributesMap.updateTimeConverted;
261 vs.updateReason = attributesMap.lastUpdateReason;
263 Position p = new Position();
264 p.lat = attributesMap.gpsLat;
265 p.lon = attributesMap.gpsLon;
266 p.heading = attributesMap.heading;
269 final List<CCMMessage> ccml = new ArrayList<CCMMessage>();
270 if (vehicleMessages != null) {
271 if (vehicleMessages.ccmMessages != null) {
272 vehicleMessages.ccmMessages.forEach(entry -> {
273 CCMMessage ccmM = new CCMMessage();
274 ccmM.ccmDescriptionShort = entry.text;
275 ccmM.ccmDescriptionLong = Constants.HYPHEN;
276 ccmM.ccmMileage = entry.unitOfLengthRemaining;
281 vs.checkControlMessages = ccml;
283 final List<CBSMessage> cbsl = new ArrayList<CBSMessage>();
284 if (vehicleMessages != null) {
285 if (vehicleMessages.cbsMessages != null) {
286 vehicleMessages.cbsMessages.forEach(entry -> {
287 CBSMessage cbsm = new CBSMessage();
288 cbsm.cbsType = entry.text;
289 cbsm.cbsDescription = entry.description;
290 cbsm.cbsDueDate = entry.date;
291 cbsm.cbsRemainingMileage = entry.unitOfLengthRemaining;
297 return Converter.getGson().toJson(vsc);
300 return Constants.EMPTY_JSON;