]> git.basschouten.com Git - openhab-addons.git/blob
0cedb5701ce25caa11a8ed40b795d146ddb8fe7f
[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.lutron.internal.discovery;
14
15 import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
16
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;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30
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;
43
44 /**
45  * The {@link LutronMcastBridgeDiscoveryService} finds RadioRA 2 Main Repeaters and HomeWorks QS
46  * Processors on the network using multicast.
47  *
48  * @author Allan Tong - Initial contribution
49  * @author Bob Adair - Renamed and added bridge properties
50  */
51 @NonNullByDefault
52 @Component(service = DiscoveryService.class, configurationPid = "discovery.lutron")
53 public class LutronMcastBridgeDiscoveryService extends AbstractDiscoveryService {
54
55     private static final int SCAN_INTERVAL_MINUTES = 30;
56     private static final int SCAN_TIMEOUT_MS = 2000;
57
58     private static final Set<ThingTypeUID> BRIDGE_TYPE_UID = Set.of(THING_TYPE_IPBRIDGE);
59
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";
66
67     private static final String DEFAULT_LABEL = "RadioRA2 MainRepeater";
68
69     private final Logger logger = LoggerFactory.getLogger(LutronMcastBridgeDiscoveryService.class);
70
71     private @Nullable ScheduledFuture<?> scanTask;
72     private @Nullable ScheduledFuture<?> backgroundScan;
73
74     public LutronMcastBridgeDiscoveryService() {
75         super(BRIDGE_TYPE_UID, 5);
76     }
77
78     @Override
79     protected void startScan() {
80         this.scanTask = scheduler.schedule(new RepeaterScanner(), 0, TimeUnit.SECONDS);
81     }
82
83     @Override
84     protected void stopScan() {
85         super.stopScan();
86
87         if (this.scanTask != null) {
88             this.scanTask.cancel(true);
89         }
90     }
91
92     @Override
93     public void abortScan() {
94         super.abortScan();
95
96         if (this.scanTask != null) {
97             this.scanTask.cancel(true);
98         }
99     }
100
101     @Override
102     protected void startBackgroundDiscovery() {
103         if (this.backgroundScan == null) {
104             this.backgroundScan = scheduler.scheduleWithFixedDelay(new RepeaterScanner(), 1, SCAN_INTERVAL_MINUTES,
105                     TimeUnit.MINUTES);
106         }
107     }
108
109     @Override
110     protected void stopBackgroundDiscovery() {
111         if (this.backgroundScan != null) {
112             this.backgroundScan.cancel(true);
113             this.backgroundScan = null;
114         }
115     }
116
117     private class RepeaterScanner implements Runnable {
118         @Override
119         public void run() {
120             try {
121                 queryForRepeaters();
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());
126             }
127         }
128
129         private void queryForRepeaters() throws IOException, InterruptedException {
130             logger.debug("Scanning for Lutron bridge devices using multicast");
131
132             InetAddress group = InetAddress.getByName(GROUP_ADDRESS);
133
134             try (MulticastSocket socket = new MulticastSocket()) {
135                 socket.setSoTimeout(SCAN_TIMEOUT_MS);
136                 socket.joinGroup(group);
137
138                 try {
139                     // Try to ensure that joinGroup has taken effect. Without this delay, the query
140                     // packet ends up going out before the group join.
141                     Thread.sleep(1000);
142
143                     socket.send(new DatagramPacket(QUERY_DATA, QUERY_DATA.length, group, QUERY_DEST_PORT));
144
145                     byte[] buf = new byte[4096];
146                     DatagramPacket packet = new DatagramPacket(buf, buf.length);
147
148                     try {
149                         while (!Thread.interrupted()) {
150                             socket.receive(packet);
151                             createBridge(packet);
152                         }
153
154                         logger.info("Bridge device scan interrupted");
155                     } catch (SocketTimeoutException e) {
156                         logger.trace(
157                                 "Timed out waiting for multicast response. Presumably all bridge devices have already responded.");
158                     }
159                 } finally {
160                     socket.leaveGroup(group);
161                 }
162             }
163         }
164
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
168             // the bridge.
169             String data = new String(packet.getData(), packet.getOffset(), packet.getLength(),
170                     StandardCharsets.US_ASCII);
171
172             Matcher matcher = BRIDGE_PROP_PATTERN.matcher(data);
173             Map<String, String> bridgeProperties = new HashMap<>();
174
175             while (matcher.find()) {
176                 bridgeProperties.put(matcher.group(1), matcher.group(2));
177                 logger.trace("Bridge property: {} : {}", matcher.group(1), matcher.group(2));
178             }
179
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");
186
187             if (ipAddress != null && !ipAddress.trim().isEmpty() && serialNumber != null
188                     && !serialNumber.trim().isEmpty()) {
189                 Map<String, Object> properties = new HashMap<>();
190
191                 properties.put(HOST, ipAddress);
192                 properties.put(SERIAL_NUMBER, serialNumber);
193
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");
198                 } else {
199                     if (productFamily != null) {
200                         properties.put(PROPERTY_PRODFAM, productFamily);
201                     }
202                 }
203
204                 if (productType != null && !productType.trim().isEmpty()) {
205                     properties.put(PROPERTY_PRODTYP, productType);
206                 }
207                 if (codeVersion != null && !codeVersion.trim().isEmpty()) {
208                     properties.put(Thing.PROPERTY_FIRMWARE_VERSION, codeVersion);
209                 }
210                 if (macAddress != null && !macAddress.trim().isEmpty()) {
211                     properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
212                 }
213
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();
218
219                 thingDiscovered(result);
220
221                 logger.debug("Discovered Lutron bridge device {}", uid);
222             }
223         }
224
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;
229             }
230
231             return DEFAULT_LABEL;
232         }
233     }
234 }