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.security.SecureRandom;
17 import java.text.SimpleDateFormat;
18 import java.time.LocalTime;
19 import java.time.ZoneId;
20 import java.time.ZoneOffset;
21 import java.time.ZonedDateTime;
22 import java.time.format.DateTimeFormatter;
23 import java.util.ArrayList;
24 import java.util.Date;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Random;
28 import java.util.TimeZone;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.mybmw.internal.MyBMWConstants;
33 import org.openhab.binding.mybmw.internal.dto.charge.Time;
34 import org.openhab.binding.mybmw.internal.dto.properties.Address;
35 import org.openhab.binding.mybmw.internal.dto.properties.Coordinates;
36 import org.openhab.binding.mybmw.internal.dto.properties.Distance;
37 import org.openhab.binding.mybmw.internal.dto.properties.Location;
38 import org.openhab.binding.mybmw.internal.dto.properties.Range;
39 import org.openhab.binding.mybmw.internal.dto.status.Mileage;
40 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.types.State;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
46 import com.google.gson.Gson;
47 import com.google.gson.JsonArray;
48 import com.google.gson.JsonObject;
49 import com.google.gson.JsonParser;
50 import com.google.gson.JsonSyntaxException;
51 import com.google.gson.reflect.TypeToken;
54 * The {@link Converter} Conversion Helpers
56 * @author Bernd Weymann - Initial contribution
59 public class Converter {
60 public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
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);
64 public static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a",
66 public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
68 private static final Gson GSON = new Gson();
69 private static final Vehicle INVALID_VEHICLE = new Vehicle();
70 private static final String SPLIT_HYPHEN = "-";
71 private static final String SPLIT_BRACKET = "\\(";
72 private static final String VIN_PATTERN = "\"vin\":";
73 private static final String VEHICLE_LOCATION_PATTERN = "\"vehicleLocation\":";
74 private static final String VEHICLE_LOCATION_REPLACEMENT = "\"vehicleLocation\": {\"coordinates\": {\"latitude\": 1.1,\"longitude\": 2.2},\"address\": {\"formatted\": \"anonymous\"},\"heading\": -1}";
75 private static final char OPEN_BRACKET = "{".charAt(0);
76 private static final char CLOSING_BRACKET = "}".charAt(0);
78 // https://www.baeldung.com/gson-list
79 public static final Type VEHICLE_LIST_TYPE = new TypeToken<ArrayList<Vehicle>>() {
81 public static int offsetMinutes = -1;
83 public static String zonedToLocalDateTime(String input) {
85 ZonedDateTime d = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault());
86 return d.toLocalDateTime().format(Converter.DATE_INPUT_PATTERN);
87 } catch (Exception e) {
88 LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage());
93 public static String toTitleCase(@Nullable String input) {
95 return toTitleCase(Constants.UNDEF);
96 } else if (input.length() == 1) {
99 String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
100 String converted = toTitleCase(lower, Constants.SPACE);
101 converted = toTitleCase(converted, SPLIT_HYPHEN);
102 converted = toTitleCase(converted, SPLIT_BRACKET);
107 private static String toTitleCase(String input, String splitter) {
108 String[] arr = input.split(splitter);
109 StringBuilder sb = new StringBuilder();
110 for (int i = 0; i < arr.length; i++) {
112 sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
114 sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
116 return sb.toString().trim();
119 public static String capitalizeFirst(String str) {
120 return str.substring(0, 1).toUpperCase() + str.substring(1);
123 public static Gson getGson() {
128 * Measure distance between 2 coordinates
130 * @param sourceLatitude
131 * @param sourceLongitude
132 * @param destinationLatitude
133 * @param destinationLongitude
136 public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
137 double destinationLongitude) {
138 double earthRadius = 6378.137; // Radius of earth in KM
139 double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
140 double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
141 double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
142 * Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
143 double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
144 return earthRadius * c;
148 * Easy function but there's some measures behind:
149 * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
150 * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
151 * line from Location A to B.
152 * I've taken some measurements to calculate the overhead factor based on Google Maps
153 * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
154 * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
155 * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
157 * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
159 * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
162 * @return mapping from air-line distance to "real road" distance
164 public static int guessRangeRadius(double range) {
165 return (int) (range * 0.8);
168 public static int getIndex(String fullString) {
171 index = Integer.parseInt(fullString);
172 } catch (NumberFormatException nfe) {
178 * Returns list of found vehicles
179 * In case of errors return empty list
184 public static List<Vehicle> getVehicleList(String json) {
186 List<Vehicle> l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
190 return new ArrayList<Vehicle>();
192 } catch (JsonSyntaxException e) {
193 LOGGER.warn("JsonSyntaxException {}", e.getMessage());
194 return new ArrayList<Vehicle>();
198 public static Vehicle getVehicle(String vin, String json) {
199 List<Vehicle> l = getVehicleList(json);
200 for (Vehicle vehicle : l) {
201 if (vin.equals(vehicle.vin)) {
202 // declare vehicle as valid
203 vehicle.valid = true;
204 return getConsistentVehcile(vehicle);
207 return INVALID_VEHICLE;
210 public static String getRawVehicleContent(String vin, String json) {
211 JsonArray jArr = JsonParser.parseString(json).getAsJsonArray();
212 for (int i = 0; i < jArr.size(); i++) {
213 JsonObject jo = jArr.get(i).getAsJsonObject();
214 String jsonVin = jo.getAsJsonPrimitive(MyBMWConstants.VIN).getAsString();
215 if (vin.equals(jsonVin)) {
216 return jo.toString();
219 return Constants.EMPTY_JSON;
223 * ensure basic data like mileage and location data are available every time
228 public static Vehicle getConsistentVehcile(Vehicle v) {
229 if (v.status.currentMileage == null) {
230 v.status.currentMileage = new Mileage();
231 v.status.currentMileage.mileage = -1;
232 v.status.currentMileage.units = "km";
234 if (v.properties.combustionRange == null) {
235 v.properties.combustionRange = new Range();
236 v.properties.combustionRange.distance = new Distance();
237 v.properties.combustionRange.distance.value = -1;
238 v.properties.combustionRange.distance.units = Constants.EMPTY;
240 if (v.properties.vehicleLocation == null) {
241 v.properties.vehicleLocation = new Location();
242 v.properties.vehicleLocation.heading = Constants.INT_UNDEF;
243 v.properties.vehicleLocation.coordinates = new Coordinates();
244 v.properties.vehicleLocation.coordinates.latitude = Constants.INT_UNDEF;
245 v.properties.vehicleLocation.coordinates.longitude = Constants.INT_UNDEF;
246 v.properties.vehicleLocation.address = new Address();
247 v.properties.vehicleLocation.address.formatted = Constants.UNDEF;
252 public static String getRandomString(int size) {
253 int leftLimit = 97; // letter 'a'
254 int rightLimit = 122; // letter 'z'
255 Random random = new SecureRandom();
257 return random.ints(leftLimit, rightLimit + 1).limit(size)
258 .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
261 public static State getLockState(boolean lock) {
263 return StringType.valueOf(Constants.LOCKED);
265 return StringType.valueOf(Constants.UNLOCKED);
269 public static State getClosedState(boolean close) {
271 return StringType.valueOf(Constants.CLOSED);
273 return StringType.valueOf(Constants.OPEN);
277 public static State getConnectionState(boolean connected) {
279 return StringType.valueOf(Constants.CONNECTED);
281 return StringType.valueOf(Constants.UNCONNECTED);
285 public static String getCurrentISOTime() {
286 Date date = new Date(System.currentTimeMillis());
287 synchronized (ISO_FORMATTER) {
288 ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
289 return ISO_FORMATTER.format(date);
293 public static String getTime(Time t) {
294 StringBuffer time = new StringBuffer();
298 time.append(Integer.toString(t.hour)).append(":");
302 time.append(Integer.toString(t.minute));
303 return time.toString();
306 public static int getOffsetMinutes() {
307 if (offsetMinutes == -1) {
308 ZoneOffset zo = ZonedDateTime.now().getOffset();
309 offsetMinutes = zo.getTotalSeconds() / 60;
311 return offsetMinutes;
314 public static int stringToInt(String intStr) {
315 int integer = Constants.INT_UNDEF;
317 integer = Integer.parseInt(intStr);
319 } catch (Exception e) {
320 LOGGER.debug("Unable to convert range {} into int value", intStr);
325 public static String getLocalTime(String chrageInfoLabel) {
326 String[] timeSplit = chrageInfoLabel.split(Constants.TILDE);
327 if (timeSplit.length == 2) {
329 LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER);
330 return timeSplit[0] + Constants.TILDE + timeL.toString();
331 } catch (Exception e) {
332 LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage());
335 return chrageInfoLabel;
338 public static String anonymousFingerprint(String raw) {
339 String anonymousFingerprintString = raw;
340 int vinStartIndex = raw.indexOf(VIN_PATTERN);
341 if (vinStartIndex != -1) {
342 String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\"");
343 String vin = arr[1].trim();
344 anonymousFingerprintString = raw.replace(vin, "anonymous");
347 int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN);
348 int bracketCounter = -1;
349 if (locationStartIndex != -1) {
350 int endLocationIndex = 0;
351 for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) {
352 endLocationIndex = i;
353 if (raw.charAt(i) == OPEN_BRACKET) {
354 if (bracketCounter == -1) {
360 } else if (raw.charAt(i) == CLOSING_BRACKET) {
364 String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1);
365 anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement,
366 VEHICLE_LOCATION_REPLACEMENT);
368 return anonymousFingerprintString;