]> git.basschouten.com Git - openhab-addons.git/blob
acedf9ac48cc7bc8586875758b7dab57a950e764
[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.lghombot.internal.discovery;
14
15 import java.io.IOException;
16 import java.net.Inet4Address;
17 import java.net.InetAddress;
18 import java.net.UnknownHostException;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.ExecutorService;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.lghombot.internal.LGHomBotBindingConstants;
30 import org.openhab.binding.lghombot.internal.LGHomBotConfiguration;
31 import org.openhab.core.config.discovery.AbstractDiscoveryService;
32 import org.openhab.core.config.discovery.DiscoveryResult;
33 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
34 import org.openhab.core.config.discovery.DiscoveryService;
35 import org.openhab.core.io.net.http.HttpUtil;
36 import org.openhab.core.net.CidrAddress;
37 import org.openhab.core.net.NetUtil;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.ThingUID;
41 import org.osgi.service.component.annotations.Component;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * Discovery class for the LG HomBot line. Right now we try to do http requests to all IPs on port 6260.
47  * If we get a connection and correct answer we set the IP as result.
48  *
49  * @author Fredrik Ahlström - Initial contribution
50  */
51 @NonNullByDefault
52 @Component(service = { DiscoveryService.class, LGHomBotDiscovery.class }, configurationPid = "discovery.lghombot")
53 public class LGHomBotDiscovery extends AbstractDiscoveryService {
54
55     private final Logger logger = LoggerFactory.getLogger(LGHomBotDiscovery.class);
56
57     /**
58      * HTTP read timeout (in milliseconds) - allows us to shutdown the listening every TIMEOUT
59      */
60     private static final int TIMEOUT_MS = 500;
61
62     /**
63      * Timeout in seconds of the complete scan
64      */
65     private static final int FULL_SCAN_TIMEOUT_SECONDS = 30;
66
67     /**
68      * Total number of concurrent threads during scanning.
69      */
70     private static final int SCAN_THREADS = 10;
71
72     /**
73      * Whether we are currently scanning or not
74      */
75     private boolean scanning;
76
77     private int octet;
78     private int ipMask;
79     private int addressCount;
80     private @Nullable CidrAddress baseIp;
81
82     /**
83      * The {@link ExecutorService} to run the listening threads on.
84      */
85     private @Nullable ExecutorService executorService;
86
87     /**
88      * Constructs the discovery class using the thing IDs that we can discover.
89      */
90     public LGHomBotDiscovery() {
91         super(LGHomBotBindingConstants.SUPPORTED_THING_TYPES_UIDS, FULL_SCAN_TIMEOUT_SECONDS, false);
92     }
93
94     private void setupBaseIp(CidrAddress adr) {
95         byte[] octets = adr.getAddress().getAddress();
96         addressCount = (1 << (32 - adr.getPrefix())) - 2;
97         ipMask = 0xFFFFFFFF << (32 - adr.getPrefix());
98         octets[0] &= ipMask >> 24;
99         octets[1] &= ipMask >> 16;
100         octets[2] &= ipMask >> 8;
101         octets[3] &= ipMask;
102         try {
103             InetAddress iAdr = InetAddress.getByAddress(octets);
104             baseIp = new CidrAddress(iAdr, (short) adr.getPrefix());
105         } catch (UnknownHostException e) {
106             logger.debug("Could not build net ip address.", e);
107         }
108         octet = 0;
109     }
110
111     private synchronized String getNextIPAddress(CidrAddress adr) {
112         octet++;
113         octet &= ~ipMask;
114         byte[] octets = adr.getAddress().getAddress();
115         octets[2] += (octet >> 8);
116         octets[3] += octet;
117         String address = "";
118         try {
119             InetAddress iAdr = null;
120             iAdr = InetAddress.getByAddress(octets);
121             address = iAdr.getHostAddress();
122         } catch (UnknownHostException e) {
123             logger.debug("Could not find next ip address.", e);
124         }
125         return address;
126     }
127
128     /**
129      * {@inheritDoc}
130      *
131      * Starts the scan. This discovery will:
132      * <ul>
133      * <li>Request this hosts first IPV4 address.</li>
134      * <li>Send a HTTP request on port 6260 to all IPs on the subnet.</li>
135      * <li>The response is then investigated to see if is an answer from a HomBot lg.srv</li>
136      * </ul>
137      * The process will continue until all addresses are checked, timeout or {@link #stopScan()} is called.
138      */
139     @Override
140     protected void startScan() {
141         if (executorService != null) {
142             stopScan();
143         }
144
145         CidrAddress localAdr = getLocalIP4Address();
146         if (localAdr == null) {
147             stopScan();
148             return;
149         }
150         setupBaseIp(localAdr);
151         CidrAddress baseAdr = baseIp;
152         scanning = true;
153         ExecutorService localExecutorService = Executors.newFixedThreadPool(SCAN_THREADS);
154         executorService = localExecutorService;
155         for (int i = 0; i < addressCount; i++) {
156
157             localExecutorService.execute(() -> {
158                 if (scanning && baseAdr != null) {
159                     String ipAdd = getNextIPAddress(baseAdr);
160                     String url = "http://" + ipAdd + ":" + LGHomBotBindingConstants.DEFAULT_HOMBOT_PORT + "/status.txt";
161
162                     try {
163                         String message = HttpUtil.executeUrl("GET", url, TIMEOUT_MS);
164                         if (message != null && !message.isEmpty()) {
165                             messageReceive(message, ipAdd);
166                         }
167                     } catch (IOException e) {
168                         // Ignore, this is the expected behavior.
169                     }
170                 }
171
172             });
173         }
174     }
175
176     /**
177      * Tries to find valid IP4 address.
178      *
179      * @return An IP4 address or null if none is found.
180      */
181     private @Nullable CidrAddress getLocalIP4Address() {
182         List<CidrAddress> adrList = NetUtil.getAllInterfaceAddresses().stream()
183                 .filter(a -> a.getAddress() instanceof Inet4Address).collect(Collectors.toList());
184
185         for (CidrAddress adr : adrList) {
186             // Don't return a "fake" DHCP lease.
187             if (!adr.toString().startsWith("169.254.")) {
188                 return adr;
189             }
190         }
191         return null;
192     }
193
194     /**
195      * lgsrv message has the following format
196      *
197      * <pre>
198      * JSON_ROBOT_STATE="CHARGING"
199      * JSON_BATTPERC="100"
200      * LGSRV_VERSION="lg.srv, V2.51 compiled 18.11.2016, by fx2"
201      * LGSRV_SUMCMD="0"
202      * LGSRV_SUMCMDSEC="0.000000"
203      * LGSRV_NUMHTTP="929"
204      * LGSRV_MEMUSAGE="0.387 MB"
205      * CPU_IDLE="67.92"
206      * CPU_USER="19.49"
207      * CPU_SYS="12.57"
208      * CPU_NICE="0.00"
209      * JSON_TURBO="false"
210      * JSON_REPEAT="false"
211      * JSON_MODE="ZZ"
212      * JSON_VERSION="16552"
213      * JSON_NICKNAME="HOMBOT"
214      * CLREC_CURRENTBUMPING="29441"
215      * CLREC_LAST_CLEAN="2018/08/30/11/00/00.826531"
216      * </pre>
217      *
218      * First parse the first string to see that it's a HomBot, then parse nickname, server version & firmware version.
219      * We then create our thing from it.
220      *
221      * @param message a response from a lgsrv to be parsed
222      * @param ipAddress current probed ip address
223      */
224     private void messageReceive(String message, String ipAddress) {
225         if (!message.startsWith("JSON_ROBOT_STATE=")) {
226             return;
227         }
228
229         String model = "HomBot";
230         String nickName = "";
231         String srvVersion = "0";
232         String fwVersion = "0";
233
234         for (String msg : message.split("\\r?\\n")) {
235             int idx = msg.indexOf('=');
236             if (idx > 0) {
237                 String name = msg.substring(0, idx);
238
239                 if ("JSON_NICKNAME".equalsIgnoreCase(name)) {
240                     nickName = msg.substring(idx + 1).trim().replace("\"", "");
241                 } else if ("JSON_VERSION".equalsIgnoreCase(name)) {
242                     fwVersion = msg.substring(idx + 1).trim().replace("\"", "");
243                 } else if ("LGSRV_VERSION".equalsIgnoreCase(name)) {
244                     srvVersion = msg.substring(idx + 1).trim().replace("\"", "");
245                 }
246             }
247
248         }
249
250         if (!ipAddress.isEmpty()) {
251             if (nickName.isEmpty()) {
252                 nickName = "HOMBOT1";
253             }
254             ThingTypeUID typeId = LGHomBotBindingConstants.THING_TYPE_LGHOMBOT;
255             ThingUID uid = new ThingUID(typeId, nickName);
256
257             Map<String, Object> properties = new HashMap<>(3);
258             properties.put(LGHomBotConfiguration.IP_ADDRESS, ipAddress);
259             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, fwVersion);
260             properties.put("server", srvVersion);
261             DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
262                     .withLabel(model + " (" + nickName + ")").build();
263             thingDiscovered(result);
264         }
265     }
266
267     /**
268      * {@inheritDoc}
269      *
270      * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
271      * within {@link #TIMEOUT_MS} * {@link #SCAN_THREADS} time then shutdown the {@link #executorService}
272      */
273     @Override
274     protected synchronized void stopScan() {
275         super.stopScan();
276         ExecutorService localExecutorService = executorService;
277         if (localExecutorService != null) {
278             scanning = false;
279             try {
280                 localExecutorService.awaitTermination(TIMEOUT_MS * SCAN_THREADS, TimeUnit.MILLISECONDS);
281             } catch (InterruptedException e) {
282                 logger.debug("Stop scan interrupted.", e);
283             }
284             localExecutorService.shutdown();
285             executorService = null;
286         }
287     }
288 }