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.utils;
15 import java.lang.reflect.Type;
16 import java.text.SimpleDateFormat;
17 import java.time.LocalTime;
18 import java.time.ZoneId;
19 import java.time.ZoneOffset;
20 import java.time.ZonedDateTime;
21 import java.time.format.DateTimeFormatter;
22 import java.util.ArrayList;
23 import java.util.Date;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.TimeZone;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.mybmw.internal.MyBMWConstants;
31 import org.openhab.binding.mybmw.internal.dto.charge.Time;
32 import org.openhab.binding.mybmw.internal.dto.properties.Address;
33 import org.openhab.binding.mybmw.internal.dto.properties.Coordinates;
34 import org.openhab.binding.mybmw.internal.dto.properties.Distance;
35 import org.openhab.binding.mybmw.internal.dto.properties.Location;
36 import org.openhab.binding.mybmw.internal.dto.properties.Range;
37 import org.openhab.binding.mybmw.internal.dto.status.Mileage;
38 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.google.gson.Gson;
45 import com.google.gson.JsonArray;
46 import com.google.gson.JsonObject;
47 import com.google.gson.JsonParser;
48 import com.google.gson.JsonSyntaxException;
49 import com.google.gson.reflect.TypeToken;
52 * The {@link Converter} Conversion Helpers
54 * @author Bernd Weymann - Initial contribution
57 public class Converter {
58 public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
60 public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
61 public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
62 public static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a",
64 public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
66 private static final Gson GSON = new Gson();
67 private static final Vehicle INVALID_VEHICLE = new Vehicle();
68 private static final String SPLIT_HYPHEN = "-";
69 private static final String SPLIT_BRACKET = "\\(";
70 private static final String VIN_PATTERN = "\"vin\":";
71 private static final String VEHICLE_LOCATION_PATTERN = "\"vehicleLocation\":";
72 private static final String VEHICLE_LOCATION_REPLACEMENT = "\"vehicleLocation\": {\"coordinates\": {\"latitude\": 1.1,\"longitude\": 2.2},\"address\": {\"formatted\": \"anonymous\"},\"heading\": -1}";
73 private static final char OPEN_BRACKET = "{".charAt(0);
74 private static final char CLOSING_BRACKET = "}".charAt(0);
76 // https://www.baeldung.com/gson-list
77 public static final Type VEHICLE_LIST_TYPE = new TypeToken<ArrayList<Vehicle>>() {
79 public static int offsetMinutes = -1;
81 public static String zonedToLocalDateTime(String input) {
83 ZonedDateTime d = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault());
84 return d.toLocalDateTime().format(Converter.DATE_INPUT_PATTERN);
85 } catch (Exception e) {
86 LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage());
91 public static String toTitleCase(@Nullable String input) {
93 return toTitleCase(Constants.UNDEF);
94 } else if (input.length() == 1) {
97 String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
98 String converted = toTitleCase(lower, Constants.SPACE);
99 converted = toTitleCase(converted, SPLIT_HYPHEN);
100 converted = toTitleCase(converted, SPLIT_BRACKET);
105 private static String toTitleCase(String input, String splitter) {
106 String[] arr = input.split(splitter);
107 StringBuilder sb = new StringBuilder();
108 for (int i = 0; i < arr.length; i++) {
110 sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
112 sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
114 return sb.toString().trim();
117 public static String capitalizeFirst(String str) {
118 return str.substring(0, 1).toUpperCase() + str.substring(1);
121 public static Gson getGson() {
126 * Measure distance between 2 coordinates
128 * @param sourceLatitude
129 * @param sourceLongitude
130 * @param destinationLatitude
131 * @param destinationLongitude
134 public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
135 double destinationLongitude) {
136 double earthRadius = 6378.137; // Radius of earth in KM
137 double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
138 double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
139 double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
140 * Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
141 double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
142 return earthRadius * c;
146 * Easy function but there's some measures behind:
147 * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
148 * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
149 * line from Location A to B.
150 * I've taken some measurements to calculate the overhead factor based on Google Maps
151 * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
152 * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
153 * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
155 * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
157 * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
160 * @return mapping from air-line distance to "real road" distance
162 public static int guessRangeRadius(double range) {
163 return (int) (range * 0.8);
166 public static int getIndex(String fullString) {
169 index = Integer.parseInt(fullString);
170 } catch (NumberFormatException nfe) {
176 * Returns list of found vehicles
177 * In case of errors return empty list
182 public static List<Vehicle> getVehicleList(String json) {
184 List<Vehicle> l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
188 return new ArrayList<Vehicle>();
190 } catch (JsonSyntaxException e) {
191 LOGGER.warn("JsonSyntaxException {}", e.getMessage());
192 return new ArrayList<Vehicle>();
196 public static Vehicle getVehicle(String vin, String json) {
197 List<Vehicle> l = getVehicleList(json);
198 for (Vehicle vehicle : l) {
199 if (vin.equals(vehicle.vin)) {
200 // declare vehicle as valid
201 vehicle.valid = true;
202 return getConsistentVehcile(vehicle);
205 return INVALID_VEHICLE;
208 public static String getRawVehicleContent(String vin, String json) {
209 JsonArray jArr = JsonParser.parseString(json).getAsJsonArray();
210 for (int i = 0; i < jArr.size(); i++) {
211 JsonObject jo = jArr.get(i).getAsJsonObject();
212 String jsonVin = jo.getAsJsonPrimitive(MyBMWConstants.VIN).getAsString();
213 if (vin.equals(jsonVin)) {
214 return jo.toString();
217 return Constants.EMPTY_JSON;
221 * ensure basic data like mileage and location data are available every time
226 public static Vehicle getConsistentVehcile(Vehicle v) {
227 if (v.status.currentMileage == null) {
228 v.status.currentMileage = new Mileage();
229 v.status.currentMileage.mileage = -1;
230 v.status.currentMileage.units = "km";
232 if (v.properties.combustionRange == null) {
233 v.properties.combustionRange = new Range();
234 v.properties.combustionRange.distance = new Distance();
235 v.properties.combustionRange.distance.value = -1;
236 v.properties.combustionRange.distance.units = Constants.EMPTY;
238 if (v.properties.vehicleLocation == null) {
239 v.properties.vehicleLocation = new Location();
240 v.properties.vehicleLocation.heading = Constants.INT_UNDEF;
241 v.properties.vehicleLocation.coordinates = new Coordinates();
242 v.properties.vehicleLocation.coordinates.latitude = Constants.INT_UNDEF;
243 v.properties.vehicleLocation.coordinates.longitude = Constants.INT_UNDEF;
244 v.properties.vehicleLocation.address = new Address();
245 v.properties.vehicleLocation.address.formatted = Constants.UNDEF;
250 public static State getLockState(boolean lock) {
252 return StringType.valueOf(Constants.LOCKED);
254 return StringType.valueOf(Constants.UNLOCKED);
258 public static State getClosedState(boolean close) {
260 return StringType.valueOf(Constants.CLOSED);
262 return StringType.valueOf(Constants.OPEN);
266 public static State getConnectionState(boolean connected) {
268 return StringType.valueOf(Constants.CONNECTED);
270 return StringType.valueOf(Constants.UNCONNECTED);
274 public static String getCurrentISOTime() {
275 Date date = new Date(System.currentTimeMillis());
276 synchronized (ISO_FORMATTER) {
277 ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
278 return ISO_FORMATTER.format(date);
282 public static String getTime(Time t) {
283 StringBuffer time = new StringBuffer();
287 time.append(Integer.toString(t.hour)).append(":");
291 time.append(Integer.toString(t.minute));
292 return time.toString();
295 public static int getOffsetMinutes() {
296 if (offsetMinutes == -1) {
297 ZoneOffset zo = ZonedDateTime.now().getOffset();
298 offsetMinutes = zo.getTotalSeconds() / 60;
300 return offsetMinutes;
303 public static int stringToInt(String intStr) {
304 int integer = Constants.INT_UNDEF;
306 integer = Integer.parseInt(intStr);
308 } catch (Exception e) {
309 LOGGER.debug("Unable to convert range {} into int value", intStr);
314 public static String getLocalTime(String chrageInfoLabel) {
315 String[] timeSplit = chrageInfoLabel.split(Constants.TILDE);
316 if (timeSplit.length == 2) {
318 LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER);
319 return timeSplit[0] + Constants.TILDE + timeL.toString();
320 } catch (Exception e) {
321 LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage());
324 return chrageInfoLabel;
327 public static String anonymousFingerprint(String raw) {
328 String anonymousFingerprintString = raw;
329 int vinStartIndex = raw.indexOf(VIN_PATTERN);
330 if (vinStartIndex != -1) {
331 String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\"");
332 String vin = arr[1].trim();
333 anonymousFingerprintString = raw.replace(vin, "anonymous");
336 int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN);
337 int bracketCounter = -1;
338 if (locationStartIndex != -1) {
339 int endLocationIndex = 0;
340 for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) {
341 endLocationIndex = i;
342 if (raw.charAt(i) == OPEN_BRACKET) {
343 if (bracketCounter == -1) {
349 } else if (raw.charAt(i) == CLOSING_BRACKET) {
353 String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1);
354 anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement,
355 VEHICLE_LOCATION_REPLACEMENT);
357 return anonymousFingerprintString;