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.Random;
27 import java.util.TimeZone;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.mybmw.internal.MyBMWConstants;
32 import org.openhab.binding.mybmw.internal.dto.charge.Time;
33 import org.openhab.binding.mybmw.internal.dto.properties.Address;
34 import org.openhab.binding.mybmw.internal.dto.properties.Coordinates;
35 import org.openhab.binding.mybmw.internal.dto.properties.Distance;
36 import org.openhab.binding.mybmw.internal.dto.properties.Location;
37 import org.openhab.binding.mybmw.internal.dto.properties.Range;
38 import org.openhab.binding.mybmw.internal.dto.status.Mileage;
39 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.types.State;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonArray;
47 import com.google.gson.JsonObject;
48 import com.google.gson.JsonParser;
49 import com.google.gson.JsonSyntaxException;
50 import com.google.gson.reflect.TypeToken;
53 * The {@link Converter} Conversion Helpers
55 * @author Bernd Weymann - Initial contribution
58 public class Converter {
59 public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
61 public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss";
62 public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING);
63 public static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a",
65 public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
67 private static final Gson GSON = new Gson();
68 private static final Vehicle INVALID_VEHICLE = new Vehicle();
69 private static final String SPLIT_HYPHEN = "-";
70 private static final String SPLIT_BRACKET = "\\(";
71 private static final String VIN_PATTERN = "\"vin\":";
72 private static final String VEHICLE_LOCATION_PATTERN = "\"vehicleLocation\":";
73 private static final String VEHICLE_LOCATION_REPLACEMENT = "\"vehicleLocation\": {\"coordinates\": {\"latitude\": 1.1,\"longitude\": 2.2},\"address\": {\"formatted\": \"anonymous\"},\"heading\": -1}";
74 private static final char OPEN_BRACKET = "{".charAt(0);
75 private static final char CLOSING_BRACKET = "}".charAt(0);
77 // https://www.baeldung.com/gson-list
78 public static final Type VEHICLE_LIST_TYPE = new TypeToken<ArrayList<Vehicle>>() {
80 public static int offsetMinutes = -1;
82 public static String zonedToLocalDateTime(String input) {
84 ZonedDateTime d = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault());
85 return d.toLocalDateTime().format(Converter.DATE_INPUT_PATTERN);
86 } catch (Exception e) {
87 LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage());
92 public static String toTitleCase(@Nullable String input) {
94 return toTitleCase(Constants.UNDEF);
95 } else if (input.length() == 1) {
98 String lower = input.replaceAll(Constants.UNDERLINE, Constants.SPACE).toLowerCase();
99 String converted = toTitleCase(lower, Constants.SPACE);
100 converted = toTitleCase(converted, SPLIT_HYPHEN);
101 converted = toTitleCase(converted, SPLIT_BRACKET);
106 private static String toTitleCase(String input, String splitter) {
107 String[] arr = input.split(splitter);
108 StringBuilder sb = new StringBuilder();
109 for (int i = 0; i < arr.length; i++) {
111 sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
113 sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
115 return sb.toString().trim();
118 public static String capitalizeFirst(String str) {
119 return str.substring(0, 1).toUpperCase() + str.substring(1);
122 public static Gson getGson() {
127 * Measure distance between 2 coordinates
129 * @param sourceLatitude
130 * @param sourceLongitude
131 * @param destinationLatitude
132 * @param destinationLongitude
135 public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude,
136 double destinationLongitude) {
137 double earthRadius = 6378.137; // Radius of earth in KM
138 double dLat = destinationLatitude * Math.PI / 180 - sourceLatitude * Math.PI / 180;
139 double dLon = destinationLongitude * Math.PI / 180 - sourceLongitude * Math.PI / 180;
140 double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(sourceLatitude * Math.PI / 180)
141 * Math.cos(destinationLatitude * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
142 double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
143 return earthRadius * c;
147 * Easy function but there's some measures behind:
148 * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
149 * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
150 * line from Location A to B.
151 * I've taken some measurements to calculate the overhead factor based on Google Maps
152 * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
153 * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
154 * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
156 * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
158 * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
161 * @return mapping from air-line distance to "real road" distance
163 public static int guessRangeRadius(double range) {
164 return (int) (range * 0.8);
167 public static int getIndex(String fullString) {
170 index = Integer.parseInt(fullString);
171 } catch (NumberFormatException nfe) {
177 * Returns list of found vehicles
178 * In case of errors return empty list
183 public static List<Vehicle> getVehicleList(String json) {
185 List<Vehicle> l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
189 return new ArrayList<Vehicle>();
191 } catch (JsonSyntaxException e) {
192 LOGGER.warn("JsonSyntaxException {}", e.getMessage());
193 return new ArrayList<Vehicle>();
197 public static Vehicle getVehicle(String vin, String json) {
198 List<Vehicle> l = getVehicleList(json);
199 for (Vehicle vehicle : l) {
200 if (vin.equals(vehicle.vin)) {
201 // declare vehicle as valid
202 vehicle.valid = true;
203 return getConsistentVehcile(vehicle);
206 return INVALID_VEHICLE;
209 public static String getRawVehicleContent(String vin, String json) {
210 JsonArray jArr = JsonParser.parseString(json).getAsJsonArray();
211 for (int i = 0; i < jArr.size(); i++) {
212 JsonObject jo = jArr.get(i).getAsJsonObject();
213 String jsonVin = jo.getAsJsonPrimitive(MyBMWConstants.VIN).getAsString();
214 if (vin.equals(jsonVin)) {
215 return jo.toString();
218 return Constants.EMPTY_JSON;
222 * ensure basic data like mileage and location data are available every time
227 public static Vehicle getConsistentVehcile(Vehicle v) {
228 if (v.status.currentMileage == null) {
229 v.status.currentMileage = new Mileage();
230 v.status.currentMileage.mileage = -1;
231 v.status.currentMileage.units = "km";
233 if (v.properties.combustionRange == null) {
234 v.properties.combustionRange = new Range();
235 v.properties.combustionRange.distance = new Distance();
236 v.properties.combustionRange.distance.value = -1;
237 v.properties.combustionRange.distance.units = Constants.EMPTY;
239 if (v.properties.vehicleLocation == null) {
240 v.properties.vehicleLocation = new Location();
241 v.properties.vehicleLocation.heading = Constants.INT_UNDEF;
242 v.properties.vehicleLocation.coordinates = new Coordinates();
243 v.properties.vehicleLocation.coordinates.latitude = Constants.INT_UNDEF;
244 v.properties.vehicleLocation.coordinates.longitude = Constants.INT_UNDEF;
245 v.properties.vehicleLocation.address = new Address();
246 v.properties.vehicleLocation.address.formatted = Constants.UNDEF;
251 public static String getRandomString(int size) {
252 int leftLimit = 97; // letter 'a'
253 int rightLimit = 122; // letter 'z'
254 Random random = new Random();
256 String generatedString = random.ints(leftLimit, rightLimit + 1).limit(size)
257 .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
259 return generatedString;
262 public static State getLockState(boolean lock) {
264 return StringType.valueOf(Constants.LOCKED);
266 return StringType.valueOf(Constants.UNLOCKED);
270 public static State getClosedState(boolean close) {
272 return StringType.valueOf(Constants.CLOSED);
274 return StringType.valueOf(Constants.OPEN);
278 public static State getConnectionState(boolean connected) {
280 return StringType.valueOf(Constants.CONNECTED);
282 return StringType.valueOf(Constants.UNCONNECTED);
286 public static String getCurrentISOTime() {
287 Date date = new Date(System.currentTimeMillis());
288 synchronized (ISO_FORMATTER) {
289 ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
290 return ISO_FORMATTER.format(date);
294 public static String getTime(Time t) {
295 StringBuffer time = new StringBuffer();
299 time.append(Integer.toString(t.hour)).append(":");
303 time.append(Integer.toString(t.minute));
304 return time.toString();
307 public static int getOffsetMinutes() {
308 if (offsetMinutes == -1) {
309 ZoneOffset zo = ZonedDateTime.now().getOffset();
310 offsetMinutes = zo.getTotalSeconds() / 60;
312 return offsetMinutes;
315 public static int stringToInt(String intStr) {
316 int integer = Constants.INT_UNDEF;
318 integer = Integer.parseInt(intStr);
320 } catch (Exception e) {
321 LOGGER.debug("Unable to convert range {} into int value", intStr);
326 public static String getLocalTime(String chrageInfoLabel) {
327 String[] timeSplit = chrageInfoLabel.split(Constants.TILDE);
328 if (timeSplit.length == 2) {
330 LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER);
331 return timeSplit[0] + Constants.TILDE + timeL.toString();
332 } catch (Exception e) {
333 LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage());
336 return chrageInfoLabel;
339 public static String anonymousFingerprint(String raw) {
340 String anonymousFingerprintString = raw;
341 int vinStartIndex = raw.indexOf(VIN_PATTERN);
342 if (vinStartIndex != -1) {
343 String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\"");
344 String vin = arr[1].trim();
345 anonymousFingerprintString = raw.replace(vin, "anonymous");
348 int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN);
349 int bracketCounter = -1;
350 if (locationStartIndex != -1) {
351 int endLocationIndex = 0;
352 for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) {
353 endLocationIndex = i;
354 if (raw.charAt(i) == OPEN_BRACKET) {
355 if (bracketCounter == -1) {
361 } else if (raw.charAt(i) == CLOSING_BRACKET) {
365 String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1);
366 anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement,
367 VEHICLE_LOCATION_REPLACEMENT);
369 return anonymousFingerprintString;