]> git.basschouten.com Git - openhab-addons.git/blob
6c43ef3cffaddf7d932a5884a59863c7f9d86292
[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
14 package org.openhab.binding.speedtest.internal;
15
16 import java.io.BufferedReader;
17 import java.io.File;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.util.Arrays;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.regex.PatternSyntaxException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.speedtest.internal.dto.ResultContainer;
29 import org.openhab.binding.speedtest.internal.dto.ResultsContainerServerList;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.QuantityType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.library.unit.Units;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
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
47 /**
48  * The {@link SpeedtestHandler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author Brian Homeyer - Initial contribution
52  */
53 @NonNullByDefault
54 public class SpeedtestHandler extends BaseThingHandler {
55     private final Logger logger = LoggerFactory.getLogger(SpeedtestHandler.class);
56     private SpeedtestConfiguration config = new SpeedtestConfiguration();
57     private Gson gson = new Gson();
58     private static Runtime rt = Runtime.getRuntime();
59     private long pollingInterval = 60;
60     private String serverID = "";
61
62     private @Nullable ScheduledFuture<?> pollingJob;
63     public volatile boolean isRunning = false;
64
65     public static final String[] SHELL_WINDOWS = new String[] { "cmd" };
66     public static final String[] SHELL_NIX = new String[] { "sh", "bash", "zsh", "csh" };
67
68     private String speedTestCommand = "";
69     private static volatile OS os = OS.NOT_SET;
70     private static final Object LOCK = new Object();
71
72     private String pingJitter = "";
73     private String pingLatency = "";
74     private String downloadBandwidth = "";
75     private String downloadBytes = "";
76     private String downloadElapsed = "";
77     private String uploadBandwidth = "";
78     private String uploadBytes = "";
79     private String uploadElapsed = "";
80     private String isp = "";
81     private String interfaceInternalIp = "";
82     private String interfaceExternalIp = "";
83     private String resultUrl = "";
84     private String server = "";
85
86     /**
87      * Contains information about which operating system openHAB is running on.
88      */
89     public enum OS {
90         WINDOWS,
91         LINUX,
92         MAC,
93         SOLARIS,
94         UNKNOWN,
95         NOT_SET
96     }
97
98     public SpeedtestHandler(Thing thing) {
99         super(thing);
100     }
101
102     @Override
103     public void handleCommand(ChannelUID channelUID, Command command) {
104         logger.debug("handleCommand channel: {} command: {}", channelUID, command);
105         String ch = channelUID.getId();
106         if (command instanceof RefreshType) {
107             if (!server.isBlank()) {
108                 updateChannels();
109             }
110             return;
111         }
112         if (ch.equals(SpeedtestBindingConstants.TRIGGER_TEST)) {
113             if (command instanceof OnOffType) {
114                 if (command == OnOffType.ON) {
115                     getSpeed();
116                     updateState(channelUID, OnOffType.OFF);
117                 }
118             }
119         }
120     }
121
122     @Override
123     public void initialize() {
124         config = getConfigAs(SpeedtestConfiguration.class);
125         pollingInterval = config.refreshInterval;
126         serverID = config.serverID;
127         if (!config.execPath.isEmpty()) {
128             speedTestCommand = config.execPath;
129         } else {
130             switch (getOperatingSystemType()) {
131                 case WINDOWS:
132                     speedTestCommand = "";
133                     break;
134                 case LINUX:
135                 case MAC:
136                 case SOLARIS:
137                     speedTestCommand = "/usr/bin/speedtest";
138                     break;
139                 default:
140                     speedTestCommand = "";
141             }
142         }
143
144         updateStatus(ThingStatus.UNKNOWN);
145
146         if (!checkConfig(speedTestCommand)) { // check the config
147             return;
148         }
149         if (!getSpeedTestVersion()) {
150             return;
151         }
152         getServerList();
153         updateStatus(ThingStatus.ONLINE);
154         isRunning = true;
155         onUpdate(); // Setup the scheduler
156     }
157
158     /**
159      * This is called to start the refresh job and also to reset that refresh job when a config change is done.
160      */
161     private void onUpdate() {
162         logger.debug("Polling Interval Set: {}", pollingInterval);
163         if (pollingInterval > 0) {
164             ScheduledFuture<?> pollingJob = this.pollingJob;
165             if (pollingJob == null || pollingJob.isCancelled()) {
166                 this.pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, pollingInterval,
167                         TimeUnit.MINUTES);
168             }
169         }
170     }
171
172     @Override
173     public void dispose() {
174         logger.debug("Disposing Speedtest Handler Thing");
175         isRunning = false;
176         ScheduledFuture<?> pollingJob = this.pollingJob;
177         if (pollingJob != null) {
178             pollingJob.cancel(true);
179             this.pollingJob = null;
180         }
181     }
182
183     /**
184      * Called when this thing gets it's configuration changed.
185      */
186     @Override
187     public void thingUpdated(Thing thing) {
188         dispose();
189         this.thing = thing;
190         initialize();
191     }
192
193     /**
194      * Polling event used to get speed data from speedtest
195      */
196     private Runnable pollingRunnable = () -> {
197         try {
198             getSpeed();
199         } catch (Exception e) {
200             logger.warn("An exception occurred while running Speedtest: '{}'", e.getMessage());
201             updateStatus(ThingStatus.OFFLINE);
202         }
203     };
204
205     /**
206      * Gets the version information from speedtest, this is really for debug in the event they change things
207      */
208     private boolean getSpeedTestVersion() {
209         String versionString = doExecuteRequest(" -V", String.class);
210         if ((versionString != null) && !versionString.isEmpty()) {
211             int newLI = versionString.indexOf(System.lineSeparator());
212             String versionLine = versionString.substring(0, newLI);
213             if (versionString.contains("Speedtest by Ookla")) {
214                 logger.debug("Speedtest Version: {}", versionLine);
215                 return true;
216             } else {
217                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
218                         "@text/offline.configuration-error.type");
219                 return false;
220             }
221         }
222         return false;
223     }
224
225     /**
226      * Get the server list from the speedtest command. Update the properties of the thing so the user
227      * can see the list of servers closest to them.
228      */
229     private boolean getServerList() {
230         String serverListTxt = "";
231         ResultsContainerServerList tmpCont = doExecuteRequest(" -f json -L", ResultsContainerServerList.class);
232         if (tmpCont != null) {
233             int id = 1;
234             Map<String, String> properties = editProperties();
235             for (ResultsContainerServerList.Server server : tmpCont.servers) {
236                 serverListTxt = "ID: " + server.id.toString() + ", " + server.host + " (" + server.location + ")";
237                 switch (id) {
238                     case 1:
239                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST1, serverListTxt);
240                         break;
241                     case 2:
242                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST2, serverListTxt);
243                         break;
244                     case 3:
245                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST3, serverListTxt);
246                         break;
247                     case 4:
248                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST4, serverListTxt);
249                         break;
250                     case 5:
251                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST5, serverListTxt);
252                         break;
253                     case 6:
254                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST6, serverListTxt);
255                         break;
256                     case 7:
257                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST7, serverListTxt);
258                         break;
259                     case 8:
260                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST8, serverListTxt);
261                         break;
262                     case 9:
263                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST9, serverListTxt);
264                         break;
265                     case 10:
266                         properties.replace(SpeedtestBindingConstants.PROPERTY_SERVER_LIST10, serverListTxt);
267                         break;
268                 }
269                 id++;
270             }
271             updateProperties(properties);
272         }
273         return false;
274     }
275
276     /**
277      * Get the speedtest data and convert it from JSON and send it to update the channels.
278      */
279     private void getSpeed() {
280         logger.debug("Getting Speed Measurement");
281         String postCommand = "";
282         if (!serverID.isBlank()) {
283             postCommand = " -s " + serverID;
284         }
285         ResultContainer tmpCont = doExecuteRequest(" -f json --accept-license --accept-gdpr" + postCommand,
286                 ResultContainer.class);
287         if (tmpCont != null) {
288             if ("result".equals(tmpCont.getType())) {
289                 pingJitter = tmpCont.getPing().getJitter();
290                 pingLatency = tmpCont.getPing().getLatency();
291                 downloadBandwidth = tmpCont.getDownload().getBandwidth();
292                 downloadBytes = tmpCont.getDownload().getBytes();
293                 downloadElapsed = tmpCont.getDownload().getElapsed();
294                 uploadBandwidth = tmpCont.getUpload().getBandwidth();
295                 uploadBytes = tmpCont.getUpload().getBytes();
296                 uploadElapsed = tmpCont.getUpload().getElapsed();
297                 isp = tmpCont.getIsp();
298                 interfaceInternalIp = tmpCont.getInterface().getInternalIp();
299                 interfaceExternalIp = tmpCont.getInterface().getExternalIp();
300                 resultUrl = tmpCont.getResult().getUrl();
301                 server = tmpCont.getServer().getName() + " (" + tmpCont.getServer().getId().toString() + ") "
302                         + tmpCont.getServer().getLocation();
303                 updateChannels();
304             }
305         } else {
306             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
307                     "@text/offline.configuration-error.results");
308         }
309     }
310
311     private @Nullable <T> T doExecuteRequest(String arguments, Class<T> type) {
312         try {
313             String dataOut = executeCmd(speedTestCommand + arguments);
314             if (type != String.class) {
315                 @Nullable
316                 T obj = gson.fromJson(dataOut, type);
317                 return obj;
318             } else {
319                 @SuppressWarnings("unchecked")
320                 T obj = (T) dataOut;
321                 return obj;
322             }
323         } catch (Exception e) {
324             logger.debug("Exception: {}", e.getMessage());
325         }
326         return null;
327     }
328
329     /**
330      * Update the channels
331      */
332     private void updateChannels() {
333         logger.debug("Updating channels");
334
335         State newState = new QuantityType<>(Double.parseDouble(pingJitter) / 1000.0, Units.SECOND);
336         logger.debug("pingJitter: {}", newState);
337         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.PING_JITTER), newState);
338
339         newState = new QuantityType<>(Double.parseDouble(pingLatency) / 1000.0, Units.SECOND);
340         logger.debug("pingLatency: {}", newState);
341         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.PING_LATENCY), newState);
342
343         newState = new QuantityType<>(Double.parseDouble(downloadBandwidth) / 125000.0, Units.MEGABIT_PER_SECOND);
344         logger.debug("downloadBandwidth: {}", newState);
345         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.DOWNLOAD_BANDWIDTH), newState);
346
347         newState = new QuantityType<>(Double.parseDouble(downloadBytes), Units.BYTE);
348         logger.debug("downloadBytes: {}", newState);
349         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.DOWNLOAD_BYTES), newState);
350
351         newState = new QuantityType<>(Double.parseDouble(downloadElapsed) / 1000.0, Units.SECOND);
352         logger.debug("downloadElapsed: {}", newState);
353         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.DOWNLOAD_ELAPSED), newState);
354
355         newState = new QuantityType<>(Double.parseDouble(uploadBandwidth) / 125000.0, Units.MEGABIT_PER_SECOND);
356         logger.debug("uploadBandwidth: {}", newState);
357         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.UPLOAD_BANDWIDTH), newState);
358
359         newState = new QuantityType<>(Double.parseDouble(uploadBytes), Units.BYTE);
360         logger.debug("uploadBytes: {}", newState);
361         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.UPLOAD_BYTES), newState);
362
363         newState = new QuantityType<>(Double.parseDouble(uploadElapsed) / 1000.0, Units.SECOND);
364         logger.debug("uploadElapsed: {}", newState);
365         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.UPLOAD_ELAPSED), newState);
366
367         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.INTERFACE_EXTERNALIP),
368                 new StringType(interfaceExternalIp));
369         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.INTERFACE_INTERNALIP),
370                 new StringType(interfaceInternalIp));
371         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.ISP), new StringType(isp));
372         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.RESULT_URL),
373                 new StringType(resultUrl));
374         updateState(new ChannelUID(getThing().getUID(), SpeedtestBindingConstants.SERVER), new StringType(server));
375     }
376
377     /**
378      * Checks to make sure the executable for speedtest is valid
379      */
380     public boolean checkConfig(String execPath) {
381         File file = new File(execPath);
382         if (!checkFileExists(file)) { // Check if entered path exists
383             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
384                     "@text/offline.configuration-error.file");
385             return false;
386         }
387
388         if (!checkFileExecutable(file)) { // Check if speedtest is executable
389             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
390                     "@text/offline.configuration-error.exec");
391             return false;
392         }
393         return true;
394     }
395
396     /**
397      * Executes a given command and returns back the String data of stdout.
398      */
399     private String executeCmd(String commandLine) {
400         int timeOut = 60000;
401         String[] cmdArray;
402         String[] shell;
403         logger.debug("Passing to shell for parsing command.");
404         switch (getOperatingSystemType()) {
405             case WINDOWS:
406                 shell = SHELL_WINDOWS;
407                 logger.debug("OS: WINDOWS ({})", getOperatingSystemName());
408                 cmdArray = createCmdArray(shell, "/c", commandLine);
409                 break;
410             case LINUX:
411             case MAC:
412             case SOLARIS:
413                 // assume sh is present, should all be POSIX-compliant
414                 shell = SHELL_NIX;
415                 logger.debug("OS: *NIX ({})", getOperatingSystemName());
416                 cmdArray = createCmdArray(shell, "-c", commandLine);
417                 break;
418             default:
419                 logger.debug("OS: Unknown ({})", getOperatingSystemName());
420                 return "";
421         }
422
423         if (cmdArray.length == 0) {
424             logger.debug("Empty command received, not executing");
425             return "";
426         }
427
428         logger.debug("The command to be executed will be '{}'", Arrays.asList(cmdArray));
429
430         Process proc;
431         try {
432             proc = rt.exec(cmdArray);
433         } catch (Exception e) {
434             logger.debug("An exception occurred while executing '{}': '{}'", Arrays.asList(cmdArray), e.getMessage());
435             return "";
436         }
437
438         StringBuilder outputBuilder = new StringBuilder();
439         try (InputStreamReader isr = new InputStreamReader(proc.getInputStream());
440                 BufferedReader br = new BufferedReader(isr)) {
441             String line;
442             while ((line = br.readLine()) != null) {
443                 outputBuilder.append(line).append(System.lineSeparator());
444                 logger.debug("Exec [{}]: '{}'", "OUTPUT", line);
445             }
446         } catch (IOException e) {
447             logger.warn("An exception occurred while reading the stdout when executing '{}': '{}'", commandLine,
448                     e.getMessage());
449         }
450
451         boolean exitVal = false;
452         try {
453             exitVal = proc.waitFor(timeOut, TimeUnit.MILLISECONDS);
454         } catch (InterruptedException e) {
455             logger.debug("An exception occurred while waiting for the process ('{}') to finish: '{}'", commandLine,
456                     e.getMessage());
457         }
458
459         if (!exitVal) {
460             logger.debug("Forcibly termininating the process ('{}') after a timeout of {} ms", commandLine, timeOut);
461             proc.destroyForcibly();
462         }
463         return outputBuilder.toString();
464     }
465
466     /**
467      * Transforms the command string into an array.
468      * Either invokes the shell and passes using the "c" option
469      * or (if command already starts with one of the shells) splits by space.
470      *
471      * @param shell (path), picks to first one to execute the command
472      * @param cOption "c"-option string
473      * @param commandLine to execute
474      * @return command array
475      */
476     protected String[] createCmdArray(String[] shell, String cOption, String commandLine) {
477         boolean startsWithShell = false;
478         for (String sh : shell) {
479             if (commandLine.startsWith(sh + " ")) {
480                 startsWithShell = true;
481                 break;
482             }
483         }
484
485         if (!startsWithShell) {
486             return new String[] { shell[0], cOption, commandLine };
487         } else {
488             logger.debug("Splitting by spaces");
489             try {
490                 return commandLine.split(" ");
491             } catch (PatternSyntaxException e) {
492                 logger.warn("An exception occurred while splitting '{}': '{}'", commandLine, e.getMessage());
493                 return new String[] {};
494             }
495         }
496     }
497
498     public static OS getOperatingSystemType() {
499         synchronized (LOCK) {
500             if (os == OS.NOT_SET) {
501                 String operSys = System.getProperty("os.name");
502                 if (operSys == null) {
503                     os = OS.UNKNOWN;
504                 } else {
505                     operSys = operSys.toLowerCase();
506
507                     if (operSys.contains("win")) {
508                         os = OS.WINDOWS;
509                     } else if (operSys.contains("nix") || operSys.contains("nux") || operSys.contains("aix")) {
510                         os = OS.LINUX;
511                     } else if (operSys.contains("mac")) {
512                         os = OS.MAC;
513                     } else if (operSys.contains("sunos")) {
514                         os = OS.SOLARIS;
515                     } else {
516                         os = OS.UNKNOWN;
517                     }
518                 }
519             }
520         }
521         return os;
522     }
523
524     public static String getOperatingSystemName() {
525         String osname = System.getProperty("os.name");
526         return osname != null ? osname : "unknown";
527     }
528
529     public boolean checkFileExists(File file) {
530         return file.exists() && !file.isDirectory();
531     }
532
533     public boolean checkFileExecutable(File file) {
534         return file.canExecute();
535     }
536 }