]> git.basschouten.com Git - openhab-addons.git/blob
9340d8b22f85a3db848bc33689298ac0f1a1a064
[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.irobot.internal.discovery;
14
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;
18
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;
31 import java.util.Set;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
34
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;
47
48 import com.google.gson.Gson;
49 import com.google.gson.stream.JsonReader;
50
51 /**
52  * Discovery service for iRobots. The {@link LoginRequester#getBlid} and
53  * {@link IRobotDiscoveryService} are heavily related to each other.
54  *
55  * @author Pavel Fedin - Initial contribution
56  * @author Alexander Falkenstern - Add support for I7 series
57  *
58  */
59 @NonNullByDefault
60 @Component(service = DiscoveryService.class, configurationPid = "discovery.irobot")
61 public class IRobotDiscoveryService extends AbstractDiscoveryService {
62
63     private final Logger logger = LoggerFactory.getLogger(IRobotDiscoveryService.class);
64
65     private final Gson gson = new Gson();
66
67     private final Runnable scanner;
68     private @Nullable ScheduledFuture<?> backgroundFuture;
69
70     public IRobotDiscoveryService() {
71         super(Collections.singleton(THING_TYPE_ROOMBA), 30, true);
72
73         scanner = createScanner();
74     }
75
76     @Override
77     protected void startBackgroundDiscovery() {
78         stopBackgroundScan();
79         backgroundFuture = scheduler.scheduleWithFixedDelay(scanner, 0, 60, TimeUnit.SECONDS);
80     }
81
82     @Override
83     protected void stopBackgroundDiscovery() {
84         stopBackgroundScan();
85         super.stopBackgroundDiscovery();
86     }
87
88     private void stopBackgroundScan() {
89         ScheduledFuture<?> scan = backgroundFuture;
90
91         if (scan != null) {
92             scan.cancel(true);
93             backgroundFuture = null;
94         }
95     }
96
97     @Override
98     protected void startScan() {
99         scheduler.execute(scanner);
100     }
101
102     private Runnable createScanner() {
103         return () -> {
104             Set<String> robots = new HashSet<>();
105             long timestampOfLastScan = getTimestampOfLastScan();
106             for (InetAddress broadcastAddress : getBroadcastAddresses()) {
107                 logger.debug("Starting broadcast for {}", broadcastAddress.toString());
108
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);
116
117                     byte @Nullable [] reply = null;
118                     while ((reply = receive(socket)) != null) {
119                         robots.add(new String(reply, StandardCharsets.UTF_8));
120                     }
121                 } catch (IOException exception) {
122                     logger.debug("Error sending broadcast: {}", exception.toString());
123                 }
124             }
125
126             for (final String json : robots) {
127
128                 JsonReader jsonReader = new JsonReader(new StringReader(json));
129                 DiscoveryResponse msg = gson.fromJson(jsonReader, DiscoveryResponse.class);
130
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);
141
142                         String name = msg.robotname;
143                         builder = builder.withLabel("iRobot " + (!name.isEmpty() ? name : UNKNOWN));
144                         thingDiscovered(builder.build());
145                     }
146                 }
147             }
148
149             removeOlderResults(timestampOfLastScan);
150         };
151     }
152
153     private byte @Nullable [] receive(DatagramSocket socket) {
154         try {
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
161             return null;
162         }
163     }
164
165     private List<InetAddress> getBroadcastAddresses() {
166         ArrayList<InetAddress> addresses = new ArrayList<>();
167
168         for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) {
169             try {
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());
175             }
176         }
177
178         return addresses;
179     }
180 }