2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lghombot.internal.discovery;
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;
22 import java.util.concurrent.ExecutorService;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
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;
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.
49 * @author Fredrik Ahlström - Initial contribution
52 @Component(service = { DiscoveryService.class, LGHomBotDiscovery.class }, configurationPid = "discovery.lghombot")
53 public class LGHomBotDiscovery extends AbstractDiscoveryService {
55 private final Logger logger = LoggerFactory.getLogger(LGHomBotDiscovery.class);
58 * HTTP read timeout (in milliseconds) - allows us to shutdown the listening every TIMEOUT
60 private static final int TIMEOUT_MS = 500;
63 * Timeout in seconds of the complete scan
65 private static final int FULL_SCAN_TIMEOUT_SECONDS = 30;
68 * Total number of concurrent threads during scanning.
70 private static final int SCAN_THREADS = 10;
73 * Whether we are currently scanning or not
75 private boolean scanning;
79 private int addressCount;
80 private @Nullable CidrAddress baseIp;
83 * The {@link ExecutorService} to run the listening threads on.
85 private @Nullable ExecutorService executorService;
88 * Constructs the discovery class using the thing IDs that we can discover.
90 public LGHomBotDiscovery() {
91 super(LGHomBotBindingConstants.SUPPORTED_THING_TYPES_UIDS, FULL_SCAN_TIMEOUT_SECONDS, false);
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;
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);
111 private synchronized String getNextIPAddress(CidrAddress adr) {
114 byte[] octets = adr.getAddress().getAddress();
115 octets[2] += (octet >> 8);
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);
131 * Starts the scan. This discovery will:
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>
137 * The process will continue until all addresses are checked, timeout or {@link #stopScan()} is called.
140 protected void startScan() {
141 if (executorService != null) {
145 CidrAddress localAdr = getLocalIP4Address();
146 if (localAdr == null) {
150 setupBaseIp(localAdr);
151 CidrAddress baseAdr = baseIp;
153 ExecutorService localExecutorService = Executors.newFixedThreadPool(SCAN_THREADS);
154 executorService = localExecutorService;
155 for (int i = 0; i < addressCount; i++) {
157 localExecutorService.execute(() -> {
158 if (scanning && baseAdr != null) {
159 String ipAdd = getNextIPAddress(baseAdr);
160 String url = "http://" + ipAdd + ":" + LGHomBotBindingConstants.DEFAULT_HOMBOT_PORT + "/status.txt";
163 String message = HttpUtil.executeUrl("GET", url, TIMEOUT_MS);
164 if (message != null && !message.isEmpty()) {
165 messageReceive(message, ipAdd);
167 } catch (IOException e) {
168 // Ignore, this is the expected behavior.
177 * Tries to find valid IP4 address.
179 * @return An IP4 address or null if none is found.
181 private @Nullable CidrAddress getLocalIP4Address() {
182 List<CidrAddress> adrList = NetUtil.getAllInterfaceAddresses().stream()
183 .filter(a -> a.getAddress() instanceof Inet4Address).collect(Collectors.toList());
185 for (CidrAddress adr : adrList) {
186 // Don't return a "fake" DHCP lease.
187 if (!adr.toString().startsWith("169.254.")) {
195 * lgsrv message has the following format
198 * JSON_ROBOT_STATE="CHARGING"
199 * JSON_BATTPERC="100"
200 * LGSRV_VERSION="lg.srv, V2.51 compiled 18.11.2016, by fx2"
202 * LGSRV_SUMCMDSEC="0.000000"
203 * LGSRV_NUMHTTP="929"
204 * LGSRV_MEMUSAGE="0.387 MB"
210 * JSON_REPEAT="false"
212 * JSON_VERSION="16552"
213 * JSON_NICKNAME="HOMBOT"
214 * CLREC_CURRENTBUMPING="29441"
215 * CLREC_LAST_CLEAN="2018/08/30/11/00/00.826531"
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.
221 * @param message a response from a lgsrv to be parsed
222 * @param ipAddress current probed ip address
224 private void messageReceive(String message, String ipAddress) {
225 if (!message.startsWith("JSON_ROBOT_STATE=")) {
229 String model = "HomBot";
230 String nickName = "";
231 String srvVersion = "0";
232 String fwVersion = "0";
234 for (String msg : message.split("\\r?\\n")) {
235 int idx = msg.indexOf('=');
237 String name = msg.substring(0, idx);
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("\"", "");
250 if (!ipAddress.isEmpty()) {
251 if (nickName.isEmpty()) {
252 nickName = "HOMBOT1";
254 ThingTypeUID typeId = LGHomBotBindingConstants.THING_TYPE_LGHOMBOT;
255 ThingUID uid = new ThingUID(typeId, nickName);
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);
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}
274 protected synchronized void stopScan() {
276 ExecutorService localExecutorService = executorService;
277 if (localExecutorService != null) {
280 localExecutorService.awaitTermination(TIMEOUT_MS * SCAN_THREADS, TimeUnit.MILLISECONDS);
281 } catch (InterruptedException e) {
282 logger.debug("Stop scan interrupted.", e);
284 localExecutorService.shutdown();
285 executorService = null;