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.lutron.internal.discovery;
15 import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.InetAddress;
20 import java.net.MulticastSocket;
21 import java.net.SocketTimeoutException;
22 import java.nio.charset.StandardCharsets;
23 import java.util.HashMap;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.core.config.discovery.AbstractDiscoveryService;
34 import org.openhab.core.config.discovery.DiscoveryResult;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.config.discovery.DiscoveryService;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
40 import org.osgi.service.component.annotations.Component;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link LutronMcastBridgeDiscoveryService} finds RadioRA 2 Main Repeaters and HomeWorks QS
46 * Processors on the network using multicast.
48 * @author Allan Tong - Initial contribution
49 * @author Bob Adair - Renamed and added bridge properties
52 @Component(service = DiscoveryService.class, configurationPid = "discovery.lutron")
53 public class LutronMcastBridgeDiscoveryService extends AbstractDiscoveryService {
55 private static final int SCAN_INTERVAL_MINUTES = 30;
56 private static final int SCAN_TIMEOUT_MS = 2000;
58 private static final Set<ThingTypeUID> BRIDGE_TYPE_UID = Set.of(THING_TYPE_IPBRIDGE);
60 private static final String GROUP_ADDRESS = "224.0.37.42";
61 private static final byte[] QUERY_DATA = "<LUTRON=1>".getBytes(StandardCharsets.US_ASCII);
62 private static final int QUERY_DEST_PORT = 2647;
63 private static final Pattern BRIDGE_PROP_PATTERN = Pattern.compile("<([^=>]+)=([^>]*)>");
64 private static final String PRODFAM_RA2 = "RadioRA2";
65 private static final String PRODFAM_HWQS = "Gulliver";
67 private static final String DEFAULT_LABEL = "RadioRA2 MainRepeater";
69 private final Logger logger = LoggerFactory.getLogger(LutronMcastBridgeDiscoveryService.class);
71 private @Nullable ScheduledFuture<?> scanTask;
72 private @Nullable ScheduledFuture<?> backgroundScan;
74 public LutronMcastBridgeDiscoveryService() {
75 super(BRIDGE_TYPE_UID, 5);
79 protected void startScan() {
80 this.scanTask = scheduler.schedule(new RepeaterScanner(), 0, TimeUnit.SECONDS);
84 protected void stopScan() {
87 if (this.scanTask != null) {
88 this.scanTask.cancel(true);
93 public void abortScan() {
96 if (this.scanTask != null) {
97 this.scanTask.cancel(true);
102 protected void startBackgroundDiscovery() {
103 if (this.backgroundScan == null) {
104 this.backgroundScan = scheduler.scheduleWithFixedDelay(new RepeaterScanner(), 1, SCAN_INTERVAL_MINUTES,
110 protected void stopBackgroundDiscovery() {
111 if (this.backgroundScan != null) {
112 this.backgroundScan.cancel(true);
113 this.backgroundScan = null;
117 private class RepeaterScanner implements Runnable {
122 } catch (InterruptedException e) {
123 logger.info("Bridge device scan interrupted");
124 } catch (IOException e) {
125 logger.warn("Communication error during bridge scan: {}", e.getMessage());
129 private void queryForRepeaters() throws IOException, InterruptedException {
130 logger.debug("Scanning for Lutron bridge devices using multicast");
132 InetAddress group = InetAddress.getByName(GROUP_ADDRESS);
134 try (MulticastSocket socket = new MulticastSocket()) {
135 socket.setSoTimeout(SCAN_TIMEOUT_MS);
136 socket.joinGroup(group);
139 // Try to ensure that joinGroup has taken effect. Without this delay, the query
140 // packet ends up going out before the group join.
143 socket.send(new DatagramPacket(QUERY_DATA, QUERY_DATA.length, group, QUERY_DEST_PORT));
145 byte[] buf = new byte[4096];
146 DatagramPacket packet = new DatagramPacket(buf, buf.length);
149 while (!Thread.interrupted()) {
150 socket.receive(packet);
151 createBridge(packet);
154 logger.info("Bridge device scan interrupted");
155 } catch (SocketTimeoutException e) {
157 "Timed out waiting for multicast response. Presumably all bridge devices have already responded.");
160 socket.leaveGroup(group);
165 private void createBridge(DatagramPacket packet) {
166 // Check response for the list of properties reported by the device. At a
167 // minimum the IP address and serial number are needed in order to create
169 String data = new String(packet.getData(), packet.getOffset(), packet.getLength(),
170 StandardCharsets.US_ASCII);
172 Matcher matcher = BRIDGE_PROP_PATTERN.matcher(data);
173 Map<String, String> bridgeProperties = new HashMap<>();
175 while (matcher.find()) {
176 bridgeProperties.put(matcher.group(1), matcher.group(2));
177 logger.trace("Bridge property: {} : {}", matcher.group(1), matcher.group(2));
180 String ipAddress = bridgeProperties.get("IPADDR");
181 String serialNumber = bridgeProperties.get("SERNUM");
182 String productFamily = bridgeProperties.get("PRODFAM");
183 String productType = bridgeProperties.get("PRODTYPE");
184 String codeVersion = bridgeProperties.get("CODEVER");
185 String macAddress = bridgeProperties.get("MACADDR");
187 if (ipAddress != null && !ipAddress.trim().isEmpty() && serialNumber != null
188 && !serialNumber.trim().isEmpty()) {
189 Map<String, Object> properties = new HashMap<>();
191 properties.put(HOST, ipAddress);
192 properties.put(SERIAL_NUMBER, serialNumber);
194 if (PRODFAM_RA2.equals(productFamily)) {
195 properties.put(PROPERTY_PRODFAM, "RadioRA 2");
196 } else if (PRODFAM_HWQS.equals(productFamily)) {
197 properties.put(PROPERTY_PRODFAM, "HomeWorks QS");
199 if (productFamily != null) {
200 properties.put(PROPERTY_PRODFAM, productFamily);
204 if (productType != null && !productType.trim().isEmpty()) {
205 properties.put(PROPERTY_PRODTYP, productType);
207 if (codeVersion != null && !codeVersion.trim().isEmpty()) {
208 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, codeVersion);
210 if (macAddress != null && !macAddress.trim().isEmpty()) {
211 properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
214 ThingUID uid = new ThingUID(THING_TYPE_IPBRIDGE, serialNumber);
215 String label = generateLabel(productFamily, productType);
216 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(label).withProperties(properties)
217 .withRepresentationProperty(SERIAL_NUMBER).build();
219 thingDiscovered(result);
221 logger.debug("Discovered Lutron bridge device {}", uid);
225 private String generateLabel(@Nullable String productFamily, @Nullable String productType) {
226 if (productFamily != null && !productFamily.trim().isEmpty() && productType != null
227 && !productType.trim().isEmpty()) {
228 return productFamily + " " + productType;
231 return DEFAULT_LABEL;