]> git.basschouten.com Git - openhab-addons.git/blob
370e92b90a1e4aaa445d6bcb8015c60eded12a29
[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.console;
14
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.BINDING_ID;
16 import static org.openhab.binding.mybmw.internal.MyBMWConstants.THING_TYPE_CONNECTED_DRIVE_ACCOUNT;
17
18 import java.io.File;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.nio.charset.StandardCharsets;
22 import java.nio.file.FileVisitResult;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.nio.file.SimpleFileVisitor;
27 import java.nio.file.attribute.BasicFileAttributes;
28 import java.time.LocalDateTime;
29 import java.time.format.DateTimeFormatter;
30 import java.util.Arrays;
31 import java.util.Comparator;
32 import java.util.List;
33 import java.util.NoSuchElementException;
34 import java.util.Objects;
35 import java.util.Optional;
36 import java.util.stream.Collectors;
37 import java.util.zip.ZipEntry;
38 import java.util.zip.ZipOutputStream;
39
40 import org.eclipse.jdt.annotation.NonNull;
41 import org.eclipse.jdt.annotation.NonNullByDefault;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase;
44 import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
45 import org.openhab.binding.mybmw.internal.handler.backend.NetworkException;
46 import org.openhab.binding.mybmw.internal.handler.backend.ResponseContentAnonymizer;
47 import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
48 import org.openhab.core.io.console.Console;
49 import org.openhab.core.io.console.ConsoleCommandCompleter;
50 import org.openhab.core.io.console.StringsCompleter;
51 import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
52 import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
53 import org.openhab.core.thing.ThingRegistry;
54 import org.openhab.core.thing.ThingStatus;
55 import org.osgi.service.component.annotations.Activate;
56 import org.osgi.service.component.annotations.Component;
57 import org.osgi.service.component.annotations.Reference;
58
59 import com.google.gson.Gson;
60 import com.google.gson.GsonBuilder;
61 import com.google.gson.JsonArray;
62 import com.google.gson.JsonElement;
63 import com.google.gson.JsonParser;
64 import com.google.gson.JsonSyntaxException;
65
66 /**
67  * The {@link MyBMWCommandExtension} is responsible for handling console commands
68  *
69  * @author Mark Herwege - Initial contribution
70  * @author Martin Grassl - improved exception handling
71  */
72
73 @NonNullByDefault
74 @Component(service = ConsoleCommandExtension.class)
75 public class MyBMWCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {
76
77     private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
78
79     private static final String FINGERPRINT_ROOT_PATH = System.getProperty("user.home") + File.separator + BINDING_ID;
80
81     private static final String FINGERPRINT = "fingerprint";
82     private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(List.of(FINGERPRINT), false);
83
84     private final ThingRegistry thingRegistry;
85
86     @Activate
87     public MyBMWCommandExtension(final @Reference ThingRegistry thingRegistry) {
88         super("mybmw", "Interact with the MyBMW binding");
89         this.thingRegistry = thingRegistry;
90     }
91
92     @Override
93     public void execute(String[] args, Console console) {
94         if ((args.length < 1) || (args.length > 3)) {
95             console.println("Invalid number of arguments");
96             printUsage(console);
97             return;
98         }
99
100         List<MyBMWBridgeHandler> bridgeHandlers = thingRegistry.stream()
101                 .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID()))
102                 .map(b -> ((MyBMWBridgeHandler) b.getHandler())).filter(Objects::nonNull).collect(Collectors.toList());
103         if (bridgeHandlers.isEmpty()) {
104             console.println("No account bridges configured");
105             return;
106         }
107
108         if (!FINGERPRINT.equalsIgnoreCase(args[0])) {
109             console.println("Unsupported command '" + args[0] + "'");
110             printUsage(console);
111             return;
112         }
113
114         List<MyBMWBridgeHandler> handlers;
115         if (args.length > 1) {
116             handlers = bridgeHandlers.stream()
117                     .filter(b -> args[1].equalsIgnoreCase(b.getThing().getConfiguration().get("userName").toString()))
118                     .filter(Objects::nonNull).collect(Collectors.toList());
119             if (handlers.isEmpty()) {
120                 console.println("No myBMW account bridge for user '" + args[1] + "'");
121                 printUsage(console);
122                 return;
123             }
124         } else {
125             handlers = bridgeHandlers;
126         }
127
128         String basePath = FINGERPRINT_ROOT_PATH + File.separator
129                 + LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE);
130         String path = nextPath(basePath, null);
131
132         console.println("# Start fingerprint");
133         int accountNdx = 0;
134         for (MyBMWBridgeHandler handler : handlers) {
135             accountNdx++;
136             console.println("### Account " + String.valueOf(accountNdx));
137             if (!ThingStatus.ONLINE.equals(handler.getThing().getStatus())) {
138                 console.println("MyBMW bridge for account not online, cannot create fingerprint");
139             } else {
140                 String accountPath = path + File.separator + "Account-" + String.valueOf(accountNdx);
141                 handler.getMyBmwProxy().ifPresentOrElse(prox -> {
142                     // get list of vehicles
143                     List<@NonNull VehicleBase> vehicles = null;
144                     try {
145                         vehicles = prox.requestVehiclesBase();
146
147                         for (String brand : BimmerConstants.REQUESTED_BRANDS) {
148                             console.println("###### Vehicles base for brand " + brand);
149                             printAndSave(console, accountPath, "VehicleBase_" + brand,
150                                     prox.requestVehiclesBaseJson(brand));
151                         }
152
153                         if (args.length == 3) {
154                             Optional<VehicleBase> vehicleOptional = vehicles.stream()
155                                     .filter(v -> v.getVin().equalsIgnoreCase(args[2])).findAny();
156                             if (vehicleOptional.isEmpty()) {
157                                 console.println("'" + args[2] + "' is not a valid vin on the account bridge with id '"
158                                         + handler.getThing().getUID().getId() + "'");
159                                 printUsage(console);
160                                 return;
161                             }
162                             vehicles = List.of(vehicleOptional.get());
163                         }
164
165                         int vinNdx = 0;
166                         for (VehicleBase vehicleBase : vehicles) {
167                             vinNdx++;
168                             String vinPath = accountPath + File.separator + "Vin-" + String.valueOf(vinNdx);
169                             console.println("###### Vehicle " + String.valueOf(vinNdx));
170
171                             // get state
172                             console.println("######## Vehicle state");
173                             printAndSave(console, vinPath, "VehicleState", prox.requestVehicleStateJson(
174                                     vehicleBase.getVin(), vehicleBase.getAttributes().getBrand()));
175
176                             // get charge statistics -> only successful for electric vehicles
177                             console.println("######### Vehicle charging statistics");
178                             printAndSave(console, vinPath, "VehicleChargingStatistics",
179                                     prox.requestChargeStatisticsJson(vehicleBase.getVin(),
180                                             vehicleBase.getAttributes().getBrand()));
181
182                             // get charge sessions -> only successful for electric vehicles
183                             console.println("######### Vehicle charging sessions");
184                             printAndSave(console, vinPath, "VehicleChargingSessions", prox.requestChargeSessionsJson(
185                                     vehicleBase.getVin(), vehicleBase.getAttributes().getBrand()));
186
187                             console.println("###### End vehicle " + String.valueOf(vinNdx));
188                         }
189                     } catch (NetworkException e) {
190                         console.println("Fingerprint failed, network exception: " + e.getReason());
191                     }
192                 }, () -> {
193                     console.println("MyBMW bridge with id '" + handler.getThing().getUID().getId()
194                             + "', communication not started, cannot retrieve fingerprint");
195                 });
196             }
197             console.println("### End account " + String.valueOf(accountNdx));
198         }
199
200         try {
201             String zipfile = nextPath(basePath, "zip");
202             zipDirectory(Paths.get(path), Paths.get(zipfile));
203             deleteDirectory(path);
204             console.println("### Fingerprint has been written to zipfile: " + zipfile);
205         } catch (IOException e) {
206             console.println("Exception zipping fingerprint: " + e.getMessage());
207             console.println("### Fingerprint has been written to files in directory: " + path);
208         }
209
210         console.println("# End fingerprint");
211     }
212
213     private void printAndSave(Console console, String path, String filename, String content) throws NetworkException {
214         String json = prettyJson(ResponseContentAnonymizer.anonymizeResponseContent(content));
215         console.println(json);
216         try {
217             writeJsonToFile(path, filename, json);
218         } catch (IOException e) {
219             console.println("Exception writing to file: " + e.getMessage());
220         }
221     }
222
223     private String nextPath(String pathString, @Nullable String extension) {
224         String path = pathString + ((extension != null) ? ("." + extension) : "");
225         int pathNdx = 1;
226         while (Files.exists(Paths.get(path))) {
227             path = pathString + "_" + String.valueOf(pathNdx) + ((extension != null) ? ("." + extension) : "");
228             pathNdx++;
229         }
230         return path;
231     }
232
233     private String prettyJson(String json) {
234         try {
235             return GSON.toJson(JsonParser.parseString(json));
236         } catch (JsonSyntaxException e) {
237             // Keep the unformatted json if there is a syntax exception
238             return json;
239         }
240     }
241
242     private void writeJsonToFile(String pathString, String filename, String json) throws IOException {
243         try {
244             JsonElement element = JsonParser.parseString(json);
245             if (element.isJsonNull() || (element.isJsonArray() && ((JsonArray) element).size() == 0)) {
246                 // Don't write a file if empty
247                 return;
248             }
249         } catch (JsonSyntaxException e) {
250             // Just continue and write the file with non-valid json anyway
251         }
252
253         String path = nextPath(pathString + File.separator + filename, "json");
254
255         // ensure full path exists
256         File file = new File(path);
257         file.getParentFile().mkdirs();
258
259         final byte[] contents = json.getBytes(StandardCharsets.UTF_8);
260         Files.write(file.toPath(), contents);
261     }
262
263     // Stackoverflow:
264     // https://stackoverflow.com/questions/57997257/how-can-i-zip-a-complete-directory-with-all-subfolders-in-java
265     private void zipDirectory(Path sourceDirectoryPath, Path zipPath) throws IOException {
266         try (FileOutputStream fos = new FileOutputStream(zipPath.toFile());
267                 ZipOutputStream zos = new ZipOutputStream(fos)) {
268             Files.walkFileTree(sourceDirectoryPath, new SimpleFileVisitor<@Nullable Path>() {
269                 @Override
270                 public FileVisitResult visitFile(@Nullable Path file, @Nullable BasicFileAttributes attrs)
271                         throws IOException {
272                     zos.putNextEntry(new ZipEntry(sourceDirectoryPath.relativize(file).toString()));
273                     Files.copy(file, zos);
274                     zos.closeEntry();
275                     return FileVisitResult.CONTINUE;
276                 }
277             });
278         } catch (IOException e) {
279             throw e;
280         }
281     }
282
283     private void deleteDirectory(String path) throws IOException {
284         Files.walk(Paths.get(path)).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
285     }
286
287     @Override
288     public List<String> getUsages() {
289         return Arrays.asList(
290                 new String[] { buildCommandUsage(FINGERPRINT, "generate fingerprint for all vehicles on all accounts"),
291                         buildCommandUsage(FINGERPRINT + " <userName>", "generate fingerprint for vehicles on account"),
292                         buildCommandUsage(FINGERPRINT + " <userName> <vin>",
293                                 "generate fingerprint for vehicle with vin on account") });
294     }
295
296     @Override
297     public @Nullable ConsoleCommandCompleter getCompleter() {
298         return this;
299     }
300
301     @Override
302     public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
303         try {
304             if (cursorArgumentIndex <= 0) {
305                 return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
306             } else if (cursorArgumentIndex == 1) {
307                 return new StringsCompleter(
308                         thingRegistry.stream()
309                                 .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID()))
310                                 .map(t -> t.getConfiguration().get("userName").toString()).collect(Collectors.toList()),
311                         false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
312             } else if (cursorArgumentIndex == 2) {
313                 MyBMWBridgeHandler handler = (MyBMWBridgeHandler) thingRegistry.stream()
314                         .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID())
315                                 && args[1].equals(t.getConfiguration().get("userName")))
316                         .map(t -> t.getHandler()).findAny().get();
317                 List<VehicleBase> vehicles = handler.getMyBmwProxy().get().requestVehiclesBase();
318                 return new StringsCompleter(
319                         vehicles.stream().map(v -> v.getVin()).filter(Objects::nonNull).collect(Collectors.toList()),
320                         false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
321             }
322         } catch (NoSuchElementException | NetworkException e) {
323             return false;
324         }
325         return false;
326     }
327 }