]> git.basschouten.com Git - openhab-addons.git/blob
d94e6a87275bf32e1283097b4a7b55e25a049c76
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mybmw.internal.utils;
14
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;
29
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;
45
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;
52
53 /**
54  * The {@link Converter} Conversion Helpers
55  *
56  * @author Bernd Weymann - Initial contribution
57  */
58 @NonNullByDefault
59 public class Converter {
60     public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class);
61
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",
65             Locale.ENGLISH);
66     public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
67
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);
77
78     // https://www.baeldung.com/gson-list
79     public static final Type VEHICLE_LIST_TYPE = new TypeToken<ArrayList<Vehicle>>() {
80     }.getType();
81     public static int offsetMinutes = -1;
82
83     public static String zonedToLocalDateTime(String input) {
84         try {
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());
89         }
90         return input;
91     }
92
93     public static String toTitleCase(@Nullable String input) {
94         if (input == null) {
95             return toTitleCase(Constants.UNDEF);
96         } else if (input.length() == 1) {
97             return input;
98         } else {
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);
103             return converted;
104         }
105     }
106
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++) {
111             if (i > 0) {
112                 sb.append(splitter.replaceAll("\\\\", Constants.EMPTY));
113             }
114             sb.append(Character.toUpperCase(arr[i].charAt(0))).append(arr[i].substring(1));
115         }
116         return sb.toString().trim();
117     }
118
119     public static String capitalizeFirst(String str) {
120         return str.substring(0, 1).toUpperCase() + str.substring(1);
121     }
122
123     public static Gson getGson() {
124         return GSON;
125     }
126
127     /**
128      * Measure distance between 2 coordinates
129      *
130      * @param sourceLatitude
131      * @param sourceLongitude
132      * @param destinationLatitude
133      * @param destinationLongitude
134      * @return distance
135      */
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;
145     }
146
147     /**
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
156      *
157      * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
158      *
159      * In future it's foreseen to replace this with BMW RangeMap Service which isn't running at the moment.
160      *
161      * @param range
162      * @return mapping from air-line distance to "real road" distance
163      */
164     public static int guessRangeRadius(double range) {
165         return (int) (range * 0.8);
166     }
167
168     public static int getIndex(String fullString) {
169         int index = -1;
170         try {
171             index = Integer.parseInt(fullString);
172         } catch (NumberFormatException nfe) {
173         }
174         return index;
175     }
176
177     /**
178      * Returns list of found vehicles
179      * In case of errors return empty list
180      *
181      * @param json
182      * @return
183      */
184     public static List<Vehicle> getVehicleList(String json) {
185         try {
186             List<Vehicle> l = GSON.fromJson(json, VEHICLE_LIST_TYPE);
187             if (l != null) {
188                 return l;
189             } else {
190                 return new ArrayList<Vehicle>();
191             }
192         } catch (JsonSyntaxException e) {
193             LOGGER.warn("JsonSyntaxException {}", e.getMessage());
194             return new ArrayList<Vehicle>();
195         }
196     }
197
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);
205             }
206         }
207         return INVALID_VEHICLE;
208     }
209
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();
217             }
218         }
219         return Constants.EMPTY_JSON;
220     }
221
222     /**
223      * ensure basic data like mileage and location data are available every time
224      *
225      * @param v
226      * @return
227      */
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";
233         }
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;
239         }
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;
248         }
249         return v;
250     }
251
252     public static String getRandomString(int size) {
253         int leftLimit = 97; // letter 'a'
254         int rightLimit = 122; // letter 'z'
255         Random random = new SecureRandom();
256
257         String generatedString = random.ints(leftLimit, rightLimit + 1).limit(size)
258                 .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
259
260         return generatedString;
261     }
262
263     public static State getLockState(boolean lock) {
264         if (lock) {
265             return StringType.valueOf(Constants.LOCKED);
266         } else {
267             return StringType.valueOf(Constants.UNLOCKED);
268         }
269     }
270
271     public static State getClosedState(boolean close) {
272         if (close) {
273             return StringType.valueOf(Constants.CLOSED);
274         } else {
275             return StringType.valueOf(Constants.OPEN);
276         }
277     }
278
279     public static State getConnectionState(boolean connected) {
280         if (connected) {
281             return StringType.valueOf(Constants.CONNECTED);
282         } else {
283             return StringType.valueOf(Constants.UNCONNECTED);
284         }
285     }
286
287     public static String getCurrentISOTime() {
288         Date date = new Date(System.currentTimeMillis());
289         synchronized (ISO_FORMATTER) {
290             ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
291             return ISO_FORMATTER.format(date);
292         }
293     }
294
295     public static String getTime(Time t) {
296         StringBuffer time = new StringBuffer();
297         if (t.hour < 10) {
298             time.append("0");
299         }
300         time.append(Integer.toString(t.hour)).append(":");
301         if (t.minute < 10) {
302             time.append("0");
303         }
304         time.append(Integer.toString(t.minute));
305         return time.toString();
306     }
307
308     public static int getOffsetMinutes() {
309         if (offsetMinutes == -1) {
310             ZoneOffset zo = ZonedDateTime.now().getOffset();
311             offsetMinutes = zo.getTotalSeconds() / 60;
312         }
313         return offsetMinutes;
314     }
315
316     public static int stringToInt(String intStr) {
317         int integer = Constants.INT_UNDEF;
318         try {
319             integer = Integer.parseInt(intStr);
320
321         } catch (Exception e) {
322             LOGGER.debug("Unable to convert range {} into int value", intStr);
323         }
324         return integer;
325     }
326
327     public static String getLocalTime(String chrageInfoLabel) {
328         String[] timeSplit = chrageInfoLabel.split(Constants.TILDE);
329         if (timeSplit.length == 2) {
330             try {
331                 LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER);
332                 return timeSplit[0] + Constants.TILDE + timeL.toString();
333             } catch (Exception e) {
334                 LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage());
335             }
336         }
337         return chrageInfoLabel;
338     }
339
340     public static String anonymousFingerprint(String raw) {
341         String anonymousFingerprintString = raw;
342         int vinStartIndex = raw.indexOf(VIN_PATTERN);
343         if (vinStartIndex != -1) {
344             String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\"");
345             String vin = arr[1].trim();
346             anonymousFingerprintString = raw.replace(vin, "anonymous");
347         }
348
349         int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN);
350         int bracketCounter = -1;
351         if (locationStartIndex != -1) {
352             int endLocationIndex = 0;
353             for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) {
354                 endLocationIndex = i;
355                 if (raw.charAt(i) == OPEN_BRACKET) {
356                     if (bracketCounter == -1) {
357                         // start point
358                         bracketCounter = 1;
359                     } else {
360                         bracketCounter++;
361                     }
362                 } else if (raw.charAt(i) == CLOSING_BRACKET) {
363                     bracketCounter--;
364                 }
365             }
366             String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1);
367             anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement,
368                     VEHICLE_LOCATION_REPLACEMENT);
369         }
370         return anonymousFingerprintString;
371     }
372 }