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.Collections;
24 import java.util.HashMap;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.core.config.discovery.AbstractDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResult;
36 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
37 import org.openhab.core.config.discovery.DiscoveryService;
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 * The {@link LutronMcastBridgeDiscoveryService} finds RadioRA 2 Main Repeaters and HomeWorks QS
47 * Processors on the network using multicast.
49 * @author Allan Tong - Initial contribution
50 * @author Bob Adair - Renamed and added bridge properties
53 @Component(service = DiscoveryService.class, configurationPid = "discovery.lutron")
54 public class LutronMcastBridgeDiscoveryService extends AbstractDiscoveryService {
56 private static final int SCAN_INTERVAL_MINUTES = 30;
57 private static final int SCAN_TIMEOUT_MS = 2000;
59 private static final Set<ThingTypeUID> BRIDGE_TYPE_UID = Collections.singleton(THING_TYPE_IPBRIDGE);
61 private static final String GROUP_ADDRESS = "224.0.37.42";
62 private static final byte[] QUERY_DATA = "<LUTRON=1>".getBytes(StandardCharsets.US_ASCII);
63 private static final int QUERY_DEST_PORT = 2647;
64 private static final Pattern BRIDGE_PROP_PATTERN = Pattern.compile("<([^=>]+)=([^>]*)>");
65 private static final String PRODFAM_RA2 = "RadioRA2";
66 private static final String PRODFAM_HWQS = "Gulliver";
68 private static final String DEFAULT_LABEL = "RadioRA2 MainRepeater";
70 private final Logger logger = LoggerFactory.getLogger(LutronMcastBridgeDiscoveryService.class);
72 private @Nullable ScheduledFuture<?> scanTask;
73 private @Nullable ScheduledFuture<?> backgroundScan;
75 public LutronMcastBridgeDiscoveryService() {
76 super(BRIDGE_TYPE_UID, 5);
80 protected void startScan() {
81 this.scanTask = scheduler.schedule(new RepeaterScanner(), 0, TimeUnit.SECONDS);
85 protected void stopScan() {
88 if (this.scanTask != null) {
89 this.scanTask.cancel(true);
94 public void abortScan() {
97 if (this.scanTask != null) {
98 this.scanTask.cancel(true);
103 protected void startBackgroundDiscovery() {
104 if (this.backgroundScan == null) {
105 this.backgroundScan = scheduler.scheduleWithFixedDelay(new RepeaterScanner(), 1, SCAN_INTERVAL_MINUTES,
111 protected void stopBackgroundDiscovery() {
112 if (this.backgroundScan != null) {
113 this.backgroundScan.cancel(true);
114 this.backgroundScan = null;
118 private class RepeaterScanner implements Runnable {
123 } catch (InterruptedException e) {
124 logger.info("Bridge device scan interrupted");
125 } catch (IOException e) {
126 logger.warn("Communication error during bridge scan: {}", e.getMessage());
130 private void queryForRepeaters() throws IOException, InterruptedException {
131 logger.debug("Scanning for Lutron bridge devices using multicast");
133 InetAddress group = InetAddress.getByName(GROUP_ADDRESS);
135 try (MulticastSocket socket = new MulticastSocket()) {
136 socket.setSoTimeout(SCAN_TIMEOUT_MS);
137 socket.joinGroup(group);
140 // Try to ensure that joinGroup has taken effect. Without this delay, the query
141 // packet ends up going out before the group join.
144 socket.send(new DatagramPacket(QUERY_DATA, QUERY_DATA.length, group, QUERY_DEST_PORT));
146 byte[] buf = new byte[4096];
147 DatagramPacket packet = new DatagramPacket(buf, buf.length);
150 while (!Thread.interrupted()) {
151 socket.receive(packet);
152 createBridge(packet);
155 logger.info("Bridge device scan interrupted");
156 } catch (SocketTimeoutException e) {
158 "Timed out waiting for multicast response. Presumably all bridge devices have already responded.");
161 socket.leaveGroup(group);
166 private void createBridge(DatagramPacket packet) {
167 // Check response for the list of properties reported by the device. At a
168 // minimum the IP address and serial number are needed in order to create
170 String data = new String(packet.getData(), packet.getOffset(), packet.getLength(),
171 StandardCharsets.US_ASCII);
173 Matcher matcher = BRIDGE_PROP_PATTERN.matcher(data);
174 Map<String, String> bridgeProperties = new HashMap<>();
176 while (matcher.find()) {
177 bridgeProperties.put(matcher.group(1), matcher.group(2));
178 logger.trace("Bridge property: {} : {}", matcher.group(1), matcher.group(2));
181 String ipAddress = bridgeProperties.get("IPADDR");
182 String serialNumber = bridgeProperties.get("SERNUM");
183 String productFamily = bridgeProperties.get("PRODFAM");
184 String productType = bridgeProperties.get("PRODTYPE");
185 String codeVersion = bridgeProperties.get("CODEVER");
186 String macAddress = bridgeProperties.get("MACADDR");
188 if (ipAddress != null && !ipAddress.trim().isEmpty() && serialNumber != null
189 && !serialNumber.trim().isEmpty()) {
190 Map<String, Object> properties = new HashMap<>();
192 properties.put(HOST, ipAddress);
193 properties.put(SERIAL_NUMBER, serialNumber);
195 if (PRODFAM_RA2.equals(productFamily)) {
196 properties.put(PROPERTY_PRODFAM, "RadioRA 2");
197 } else if (PRODFAM_HWQS.equals(productFamily)) {
198 properties.put(PROPERTY_PRODFAM, "HomeWorks QS");
200 if (productFamily != null) {
201 properties.put(PROPERTY_PRODFAM, productFamily);
205 if (productType != null && !productType.trim().isEmpty()) {
206 properties.put(PROPERTY_PRODTYP, productType);
208 if (codeVersion != null && !codeVersion.trim().isEmpty()) {
209 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, codeVersion);
211 if (macAddress != null && !macAddress.trim().isEmpty()) {
212 properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
215 ThingUID uid = new ThingUID(THING_TYPE_IPBRIDGE, serialNumber);
216 String label = generateLabel(productFamily, productType);
217 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(label).withProperties(properties)
218 .withRepresentationProperty(SERIAL_NUMBER).build();
220 thingDiscovered(result);
222 logger.debug("Discovered Lutron bridge device {}", uid);
226 private String generateLabel(@Nullable String productFamily, @Nullable String productType) {
227 if (productFamily != null && !productFamily.trim().isEmpty() && productType != null
228 && !productType.trim().isEmpty()) {
229 return productFamily + " " + productType;
232 return DEFAULT_LABEL;