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