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