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.russound.internal.discovery;
15 import java.io.IOException;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
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;
38 * This implementation of {@link DiscoveryService} will scan a RIO device for all controllers, source and zones attached
41 * @author Tim Roberts - Initial contribution
43 public class RioSystemDeviceDiscoveryService extends AbstractDiscoveryService {
45 private final Logger logger = LoggerFactory.getLogger(RioSystemDeviceDiscoveryService.class);
47 /** The system handler to scan */
48 private final RioSystemHandler sysHandler;
50 /** Pattern to identify controller notifications */
51 private static final Pattern RSP_CONTROLLERNOTIFICATION = Pattern
52 .compile("(?i)^[SN] C\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
54 /** Pattern to identify source notifications */
55 private static final Pattern RSP_SRCNOTIFICATION = Pattern.compile("(?i)^[SN] S\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
57 /** Pattern to identify zone notifications */
58 private static final Pattern RSP_ZONENOTIFICATION = Pattern
59 .compile("(?i)^[SN] C\\[(\\d+)\\]\\.Z\\[(\\d+)\\]\\.(\\w+)=\"(.*)\"$");
62 * The {@link SocketSession} that will be used to scan the device
64 private SocketSession session;
67 * The {@link WaitingSessionListener} to the {@link #session} to receive/process responses
69 private WaitingSessionListener listener;
72 * Create the discovery service from the {@link RioSystemHandler}
74 * @param sysHandler a non-null {@link RioSystemHandler}
75 * @throws IllegalArgumentException if sysHandler is null
77 public RioSystemDeviceDiscoveryService(RioSystemHandler sysHandler) {
78 super(RussoundHandlerFactory.SUPPORTED_THING_TYPES_UIDS, 30, false);
80 if (sysHandler == null) {
81 throw new IllegalArgumentException("sysHandler can't be null");
83 this.sysHandler = sysHandler;
87 * Activates this discovery service. Simply registers this with
88 * {@link RioSystemHandler#registerDiscoveryService(RioSystemDeviceDiscoveryService)}
90 public void activate() {
91 sysHandler.registerDiscoveryService(this);
95 * Deactivates the scan - will disconnect the session and remove the {@link #listener}
98 public void deactivate() {
99 if (session != null) {
101 session.disconnect();
102 } catch (IOException e) {
105 session.removeListener(listener);
112 * Overridden to do nothing - {@link #scanDevice()} is called by {@link RioSystemHandler} instead
115 protected void startScan() {
116 // do nothing - started by RioSystemHandler
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()}
123 public void scanDevice() {
125 final String ipAddress = sysHandler.getRioConfig().getIpAddress();
126 session = new SocketChannelSession(ipAddress, RioConstants.RIO_PORT);
127 listener = new WaitingSessionListener();
128 session.addListener(listener);
131 logger.debug("Starting scan of RIO device at {}", ipAddress);
133 discoverControllers();
135 } catch (IOException e) {
136 logger.debug("Trying to scan device but couldn't connect: {}", e.getMessage(), e);
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)}
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);
154 final ThingUID thingUID = new ThingUID(RioConstants.BRIDGE_TYPE_CONTROLLER,
155 sysHandler.getThing().getUID(), String.valueOf(c));
157 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
158 .withProperty(RioControllerConfig.CONTROLLER, c).withBridge(sysHandler.getThing().getUID())
159 .withLabel("Controller #" + c).build();
160 thingDiscovered(discoveryResult);
162 discoverZones(thingUID, c);
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.
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);
179 final ThingUID thingUID = new ThingUID(RioConstants.THING_TYPE_SOURCE, sysHandler.getThing().getUID(),
182 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
183 .withProperty(RioSourceConfig.SOURCE, s).withBridge(sysHandler.getThing().getUID())
184 .withLabel((name == null || name.isEmpty() || name.equalsIgnoreCase("null") ? "Source" : name)
187 thingDiscovered(discoveryResult);
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.
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
201 private void discoverZones(ThingUID controllerUID, int c) {
202 if (controllerUID == null) {
203 throw new IllegalArgumentException("controllerUID cannot be null");
205 if (c < 1 || c > 8) {
206 throw new IllegalArgumentException("c must be between 1 and 8");
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);
213 final ThingUID thingUID = new ThingUID(RioConstants.THING_TYPE_ZONE, controllerUID, String.valueOf(z));
215 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
216 .withProperty(RioZoneConfig.ZONE, z).withBridge(controllerUID)
217 .withLabel((name.equalsIgnoreCase("null") ? "Zone" : name) + " (" + z + ")").build();
218 thingDiscovered(discoveryResult);
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.
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
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");
239 if (respPattern == null) {
240 throw new IllegalArgumentException("respPattern cannot be null");
243 throw new IllegalArgumentException("groupNum must be >= 0");
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);
253 logger.debug("Message '{}' returned an invalid response: {}", message, r);
255 } catch (InterruptedException e) {
256 logger.debug("Sending message '{}' was interrupted and could not be completed", message);
258 } catch (IOException e) {
259 logger.debug("Sending message '{}' resulted in an IOException and could not be completed: {}", message,