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.irobot.internal.discovery;
15 import static org.openhab.binding.irobot.internal.IRobotBindingConstants.THING_TYPE_ROOMBA;
16 import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UDP_PORT;
17 import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UNKNOWN;
19 import java.io.IOException;
20 import java.io.StringReader;
21 import java.net.DatagramPacket;
22 import java.net.DatagramSocket;
23 import java.net.InetAddress;
24 import java.net.UnknownHostException;
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.HashSet;
30 import java.util.List;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.irobot.internal.dto.MQTTProtocol.DiscoveryResponse;
38 import org.openhab.binding.irobot.internal.utils.LoginRequester;
39 import org.openhab.core.config.discovery.AbstractDiscoveryService;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.DiscoveryService;
42 import org.openhab.core.net.NetUtil;
43 import org.openhab.core.thing.ThingUID;
44 import org.osgi.service.component.annotations.Component;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.google.gson.Gson;
49 import com.google.gson.stream.JsonReader;
52 * Discovery service for iRobots. The {@link LoginRequester#getBlid} and
53 * {@link IRobotDiscoveryService} are heavily related to each other.
55 * @author Pavel Fedin - Initial contribution
56 * @author Alexander Falkenstern - Add support for I7 series
60 @Component(service = DiscoveryService.class, configurationPid = "discovery.irobot")
61 public class IRobotDiscoveryService extends AbstractDiscoveryService {
63 private final Logger logger = LoggerFactory.getLogger(IRobotDiscoveryService.class);
65 private final Gson gson = new Gson();
67 private final Runnable scanner;
68 private @Nullable ScheduledFuture<?> backgroundFuture;
70 public IRobotDiscoveryService() {
71 super(Collections.singleton(THING_TYPE_ROOMBA), 30, true);
73 scanner = createScanner();
77 protected void startBackgroundDiscovery() {
79 backgroundFuture = scheduler.scheduleWithFixedDelay(scanner, 0, 60, TimeUnit.SECONDS);
83 protected void stopBackgroundDiscovery() {
85 super.stopBackgroundDiscovery();
88 private void stopBackgroundScan() {
89 ScheduledFuture<?> scan = backgroundFuture;
93 backgroundFuture = null;
98 protected void startScan() {
99 scheduler.execute(scanner);
102 private Runnable createScanner() {
104 Set<String> robots = new HashSet<>();
105 long timestampOfLastScan = getTimestampOfLastScan();
106 for (InetAddress broadcastAddress : getBroadcastAddresses()) {
107 logger.debug("Starting broadcast for {}", broadcastAddress.toString());
109 final byte[] bRequest = "irobotmcs".getBytes(StandardCharsets.UTF_8);
110 DatagramPacket request = new DatagramPacket(bRequest, bRequest.length, broadcastAddress, UDP_PORT);
111 try (DatagramSocket socket = new DatagramSocket()) {
112 socket.setSoTimeout(1000); // One second
113 socket.setReuseAddress(true);
114 socket.setBroadcast(true);
115 socket.send(request);
117 byte @Nullable [] reply = null;
118 while ((reply = receive(socket)) != null) {
119 robots.add(new String(reply, StandardCharsets.UTF_8));
121 } catch (IOException exception) {
122 logger.debug("Error sending broadcast: {}", exception.toString());
126 for (final String json : robots) {
128 JsonReader jsonReader = new JsonReader(new StringReader(json));
129 DiscoveryResponse msg = gson.fromJson(jsonReader, DiscoveryResponse.class);
131 // Only firmware version 2 and above are supported via MQTT, therefore check it
132 if ((msg.ver != null) && (Integer.parseInt(msg.ver) > 1) && "mqtt".equalsIgnoreCase(msg.proto)) {
133 final String address = msg.ip;
134 final String mac = msg.mac;
135 final String sku = msg.sku;
136 if (!address.isEmpty() && !sku.isEmpty() && !mac.isEmpty()) {
137 ThingUID thingUID = new ThingUID(THING_TYPE_ROOMBA, mac.replace(":", ""));
138 DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID);
139 builder = builder.withProperty("mac", mac).withRepresentationProperty("mac");
140 builder = builder.withProperty("ipaddress", address);
142 String name = msg.robotname;
143 builder = builder.withLabel("iRobot " + (!name.isEmpty() ? name : UNKNOWN));
144 thingDiscovered(builder.build());
149 removeOlderResults(timestampOfLastScan);
153 private byte @Nullable [] receive(DatagramSocket socket) {
155 final byte[] bReply = new byte[1024];
156 DatagramPacket reply = new DatagramPacket(bReply, bReply.length);
157 socket.receive(reply);
158 return Arrays.copyOfRange(reply.getData(), reply.getOffset(), reply.getLength());
159 } catch (IOException exception) {
160 // This is not really an error, eventually we get a timeout due to a loop in the caller
165 private List<InetAddress> getBroadcastAddresses() {
166 ArrayList<InetAddress> addresses = new ArrayList<>();
168 for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) {
170 addresses.add(InetAddress.getByName(broadcastAddress));
171 } catch (UnknownHostException exception) {
172 // The broadcastAddress is supposed to be raw IP, not a hostname, like 192.168.0.255.
173 // Getting UnknownHost on it would be totally strange, some internal system error.
174 logger.warn("Error broadcasting to {}: {}", broadcastAddress, exception.getMessage());