]> git.basschouten.com Git - openhab-addons.git/blob
7537044518b34b4c387e6bdfa46971883fc34c3b
[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.russound.internal.discovery;
14
15 import java.io.IOException;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
18
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.russound.internal.RussoundHandlerFactory;
21 import org.openhab.binding.russound.internal.net.SocketChannelSession;
22 import org.openhab.binding.russound.internal.net.SocketSession;
23 import org.openhab.binding.russound.internal.net.WaitingSessionListener;
24 import org.openhab.binding.russound.internal.rio.RioConstants;
25 import org.openhab.binding.russound.internal.rio.controller.RioControllerConfig;
26 import org.openhab.binding.russound.internal.rio.source.RioSourceConfig;
27 import org.openhab.binding.russound.internal.rio.system.RioSystemHandler;
28 import org.openhab.binding.russound.internal.rio.zone.RioZoneConfig;
29 import org.openhab.core.config.discovery.AbstractDiscoveryService;
30 import org.openhab.core.config.discovery.DiscoveryResult;
31 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
32 import org.openhab.core.config.discovery.DiscoveryService;
33 import org.openhab.core.thing.ThingUID;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38  * This implementation of {@link DiscoveryService} will scan a RIO device for all controllers, source and zones attached
39  * to it.
40  *
41  * @author Tim Roberts - Initial contribution
42  */
43 public class RioSystemDeviceDiscoveryService extends AbstractDiscoveryService {
44     /** The logger */
45     private final Logger logger = LoggerFactory.getLogger(RioSystemDeviceDiscoveryService.class);
46
47     /** The system handler to scan */
48     private final RioSystemHandler sysHandler;
49
50     /** Pattern to identify controller notifications */
51     private static final Pattern RSP_CONTROLLERNOTIFICATION = Pattern
52             .compile("(?i)^[SN] C\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
53
54     /** Pattern to identify source notifications */
55     private static final Pattern RSP_SRCNOTIFICATION = Pattern.compile("(?i)^[SN] S\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
56
57     /** Pattern to identify zone notifications */
58     private static final Pattern RSP_ZONENOTIFICATION = Pattern
59             .compile("(?i)^[SN] C\\[(\\d+)\\]\\.Z\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
60
61     /**
62      * The {@link SocketSession} that will be used to scan the device
63      */
64     private SocketSession session;
65
66     /**
67      * The {@link WaitingSessionListener} to the {@link #session} to receive/process responses
68      */
69     private WaitingSessionListener listener;
70
71     /**
72      * Create the discovery service from the {@link RioSystemHandler}
73      *
74      * @param sysHandler a non-null {@link RioSystemHandler}
75      * @throws IllegalArgumentException if sysHandler is null
76      */
77     public RioSystemDeviceDiscoveryService(RioSystemHandler sysHandler) {
78         super(RussoundHandlerFactory.SUPPORTED_THING_TYPES_UIDS, 30, false);
79
80         if (sysHandler == null) {
81             throw new IllegalArgumentException("sysHandler can't be null");
82         }
83         this.sysHandler = sysHandler;
84     }
85
86     /**
87      * Activates this discovery service. Simply registers this with
88      * {@link RioSystemHandler#registerDiscoveryService(RioSystemDeviceDiscoveryService)}
89      */
90     public void activate() {
91         sysHandler.registerDiscoveryService(this);
92     }
93
94     /**
95      * Deactivates the scan - will disconnect the session and remove the {@link #listener}
96      */
97     @Override
98     public void deactivate() {
99         if (session != null) {
100             try {
101                 session.disconnect();
102             } catch (IOException e) {
103                 // ignore
104             }
105             session.removeListener(listener);
106             session = null;
107             listener = null;
108         }
109     }
110
111     /**
112      * Overridden to do nothing - {@link #scanDevice()} is called by {@link RioSystemHandler} instead
113      */
114     @Override
115     protected void startScan() {
116         // do nothing - started by RioSystemHandler
117     }
118
119     /**
120      * Starts a device scan. This will connect to the device and discover the controllers/sources/zones attached to the
121      * device and then disconnect via {@link #deactivate()}
122      */
123     public void scanDevice() {
124         try {
125             final String ipAddress = sysHandler.getRioConfig().getIpAddress();
126             session = new SocketChannelSession(ipAddress, RioConstants.RIO_PORT);
127             listener = new WaitingSessionListener();
128             session.addListener(listener);
129
130             try {
131                 logger.debug("Starting scan of RIO device at {}", ipAddress);
132                 session.connect();
133                 discoverControllers();
134                 discoverSources();
135             } catch (IOException e) {
136                 logger.debug("Trying to scan device but couldn't connect: {}", e.getMessage(), e);
137             }
138         } finally {
139             deactivate();
140         }
141     }
142
143     /**
144      * Helper method to discover controllers - this will iterate through all possible controllers (6 of them )and see if
145      * any respond to the "type" command. If they do, we initiate a {@link #thingDiscovered(DiscoveryResult)} for the
146      * controller and then scan the controller for zones via {@link #discoverZones(ThingUID, int)}
147      */
148     private void discoverControllers() {
149         for (int c = 1; c < 7; c++) {
150             final String type = sendAndGet("GET C[" + c + "].type", RSP_CONTROLLERNOTIFICATION, 3);
151             if (type != null && !type.isEmpty()) {
152                 logger.debug("Controller #{} found - {}", c, type);
153
154                 final ThingUID thingUID = new ThingUID(RioConstants.BRIDGE_TYPE_CONTROLLER,
155                         sysHandler.getThing().getUID(), String.valueOf(c));
156
157                 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
158                         .withProperty(RioControllerConfig.CONTROLLER, c).withBridge(sysHandler.getThing().getUID())
159                         .withLabel("Controller #" + c).build();
160                 thingDiscovered(discoveryResult);
161
162                 discoverZones(thingUID, c);
163             }
164         }
165     }
166
167     /**
168      * Helper method to discover sources. This will iterate through all possible sources (8 of them) and see if they
169      * respond to the "type" command. If they do, we retrieve the source "name" and initial a
170      * {@link #thingDiscovered(DiscoveryResult)} for the source.
171      */
172     private void discoverSources() {
173         for (int s = 1; s < 9; s++) {
174             final String type = sendAndGet("GET S[" + s + "].type", RSP_SRCNOTIFICATION, 3);
175             if (type != null && !type.isEmpty()) {
176                 final String name = sendAndGet("GET S[" + s + "].name", RSP_SRCNOTIFICATION, 3);
177                 logger.debug("Source #{} - {}/{}", s, type, name);
178
179                 final ThingUID thingUID = new ThingUID(RioConstants.THING_TYPE_SOURCE, sysHandler.getThing().getUID(),
180                         String.valueOf(s));
181
182                 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
183                         .withProperty(RioSourceConfig.SOURCE, s).withBridge(sysHandler.getThing().getUID())
184                         .withLabel((name == null || name.isEmpty() || "null".equalsIgnoreCase(name) ? "Source" : name)
185                                 + " (" + s + ")")
186                         .build();
187                 thingDiscovered(discoveryResult);
188             }
189         }
190     }
191
192     /**
193      * Helper method to discover zones. This will iterate through all possible zones (8 of them) and see if they
194      * respond to the "name" command. If they do, initial a {@link #thingDiscovered(DiscoveryResult)} for the zone.
195      *
196      * @param controllerUID the {@link ThingUID} of the parent controller
197      * @param c the controller identifier
198      * @throws IllegalArgumentException if controllerUID is null
199      * @throws IllegalArgumentException if c is < 1 or > 8
200      */
201     private void discoverZones(ThingUID controllerUID, int c) {
202         if (controllerUID == null) {
203             throw new IllegalArgumentException("controllerUID cannot be null");
204         }
205         if (c < 1 || c > 8) {
206             throw new IllegalArgumentException("c must be between 1 and 8");
207         }
208         for (int z = 1; z < 9; z++) {
209             final String name = sendAndGet("GET C[" + c + "].Z[" + z + "].name", RSP_ZONENOTIFICATION, 4);
210             if (name != null && !name.isEmpty()) {
211                 logger.debug("Controller #{}, Zone #{} found - {}", c, z, name);
212
213                 final ThingUID thingUID = new ThingUID(RioConstants.THING_TYPE_ZONE, controllerUID, String.valueOf(z));
214
215                 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
216                         .withProperty(RioZoneConfig.ZONE, z).withBridge(controllerUID)
217                         .withLabel(("null".equalsIgnoreCase(name) ? "Zone" : name) + " (" + z + ")").build();
218                 thingDiscovered(discoveryResult);
219             }
220         }
221     }
222
223     /**
224      * Helper method to send a message, parse the result with the given {@link Pattern} and extract the data in the
225      * specified group number.
226      *
227      * @param message the message to send
228      * @param respPattern the response pattern to apply
229      * @param groupNum the group # to return
230      * @return a possibly null response (null if an exception occurs or the response isn't a match or the response
231      *         doesn't have the right amount of groups)
232      * @throws IllegalArgumentException if message is null or empty, if the pattern is null
233      * @throws IllegalArgumentException if groupNum is less than 0
234      */
235     private @Nullable String sendAndGet(String message, Pattern respPattern, int groupNum) {
236         if (message == null || message.isEmpty()) {
237             throw new IllegalArgumentException("message cannot be a null or empty string");
238         }
239         if (respPattern == null) {
240             throw new IllegalArgumentException("respPattern cannot be null");
241         }
242         if (groupNum < 0) {
243             throw new IllegalArgumentException("groupNum must be >= 0");
244         }
245         try {
246             session.sendCommand(message);
247             final String r = listener.getResponse();
248             final Matcher m = respPattern.matcher(r);
249             if (m.matches() && m.groupCount() >= groupNum) {
250                 logger.debug("Message '{}' returned a valid response: {}", message, r);
251                 return m.group(groupNum);
252             }
253             logger.debug("Message '{}' returned an invalid response: {}", message, r);
254             return null;
255         } catch (InterruptedException e) {
256             logger.debug("Sending message '{}' was interrupted and could not be completed", message);
257             return null;
258         } catch (IOException e) {
259             logger.debug("Sending message '{}' resulted in an IOException and could not be completed: {}", message,
260                     e.getMessage(), e);
261             return null;
262         }
263     }
264 }