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.anel.internal.discovery;
15 import java.io.IOException;
16 import java.net.BindException;
17 import java.nio.channels.ClosedByInterruptException;
19 import java.util.TreeSet;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.anel.internal.AnelUdpConnector;
24 import org.openhab.binding.anel.internal.IAnelConstants;
25 import org.openhab.core.common.AbstractUID;
26 import org.openhab.core.common.NamedThreadFactory;
27 import org.openhab.core.config.discovery.AbstractDiscoveryService;
28 import org.openhab.core.config.discovery.DiscoveryResult;
29 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
30 import org.openhab.core.config.discovery.DiscoveryService;
31 import org.openhab.core.net.NetUtil;
32 import org.openhab.core.thing.ThingTypeUID;
33 import org.openhab.core.thing.ThingUID;
34 import org.osgi.service.component.annotations.Component;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * Discovery service for ANEL devices.
41 * @author Patrick Koenemann - Initial contribution
44 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.anel")
45 public class AnelDiscoveryService extends AbstractDiscoveryService {
47 private static final String PASSWORD = "anel";
48 private static final String USER = "user7";
49 private static final int[][] DISCOVERY_PORTS = { { 750, 770 }, { 7500, 7700 }, { 7750, 7770 } };
50 private static final Set<String> BROADCAST_ADDRESSES = new TreeSet<>(NetUtil.getAllBroadcastAddresses());
52 private static final int DISCOVER_DEVICE_TIMEOUT_SECONDS = 2;
54 /** #BroadcastAddresses * DiscoverDeviceTimeout * (3 * #DiscoveryPorts) */
55 private static final int DISCOVER_TIMEOUT_SECONDS = BROADCAST_ADDRESSES.size() * DISCOVER_DEVICE_TIMEOUT_SECONDS
56 * (3 * DISCOVERY_PORTS.length);
58 private final Logger logger = LoggerFactory.getLogger(AnelDiscoveryService.class);
60 private @Nullable Thread scanningThread = null;
62 public AnelDiscoveryService() throws IllegalArgumentException {
63 super(IAnelConstants.SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS);
65 "Anel NET-PwrCtrl discovery service instantiated for broadcast addresses {} with a timeout of {} seconds.",
66 BROADCAST_ADDRESSES, DISCOVER_TIMEOUT_SECONDS);
70 protected void startScan() {
72 * Start scan in background thread, otherwise progress is not shown in the web UI.
73 * Do not use the scheduler, otherwise further threads (for handling discovered things) are not started
74 * immediately but only after the scan is complete.
76 final Thread thread = new NamedThreadFactory(IAnelConstants.BINDING_ID, true).newThread(this::doScan);
78 scanningThread = thread;
81 private void doScan() {
82 logger.debug("Starting scan of Anel devices via UDP broadcast messages...");
85 for (final String broadcastAddress : BROADCAST_ADDRESSES) {
87 // for each available broadcast network address try factory default ports first
88 scan(broadcastAddress, IAnelConstants.DEFAULT_SEND_PORT, IAnelConstants.DEFAULT_RECEIVE_PORT);
90 // try reasonable ports...
91 for (int[] ports : DISCOVERY_PORTS) {
92 int sendPort = ports[0];
93 int receivePort = ports[1];
95 // ...and continue if a device was found, maybe there is yet another device on the next port
96 while (scan(broadcastAddress, sendPort, receivePort) || sendPort == ports[0]) {
102 } catch (InterruptedException | ClosedByInterruptException e) {
103 return; // OH shutdown or scan was aborted
104 } catch (Exception e) {
105 logger.warn("Unexpected exception during anel device scan", e);
107 scanningThread = null;
109 logger.debug("Scan finished.");
112 /* @return Whether or not a device was found for the given broadcast address and port. */
113 private boolean scan(String broadcastAddress, int sendPort, int receivePort)
114 throws IOException, InterruptedException {
115 logger.debug("Scanning {}:{}...", broadcastAddress, sendPort);
116 final AnelUdpConnector udpConnector = new AnelUdpConnector(broadcastAddress, receivePort, sendPort, scheduler);
119 final boolean[] deviceDiscovered = new boolean[] { false };
120 udpConnector.connect(status -> {
121 // avoid the same device to be discovered multiple times for multiple responses
122 if (!deviceDiscovered[0]) {
123 boolean discoverDevice = true;
124 synchronized (this) {
125 if (deviceDiscovered[0]) {
126 discoverDevice = false; // already discovered by another thread
128 deviceDiscovered[0] = true; // we discover the device!
131 if (discoverDevice) {
132 // discover device outside synchronized-block
133 deviceDiscovered(status, sendPort, receivePort);
138 udpConnector.send(IAnelConstants.BROADCAST_DISCOVERY_MSG);
140 // answer expected within 50-600ms on a regular network; wait up to 2sec just to make sure
141 for (int delay = 0; delay < 10 && !deviceDiscovered[0]; delay++) {
142 Thread.sleep(100 * DISCOVER_DEVICE_TIMEOUT_SECONDS); // wait 10 x 200ms = 2sec
145 return deviceDiscovered[0];
146 } catch (BindException e) {
147 // most likely socket is already in use, ignore this exception.
149 "Invalid address {} or one of the ports {} or {} is already in use. Skipping scan of these ports.",
150 broadcastAddress, sendPort, receivePort);
152 udpConnector.disconnect();
158 protected synchronized void stopScan() {
159 final Thread thread = scanningThread;
160 if (thread != null) {
166 private void deviceDiscovered(String status, int sendPort, int receivePort) {
167 final String[] segments = status.split(":");
168 if (segments.length >= 16) {
169 final String name = segments[1].trim();
170 final String ip = segments[2];
171 final String macAddress = segments[5];
172 final String deviceType = segments.length > 17 ? segments[17] : null;
173 final ThingTypeUID thingTypeUid = getThingTypeUid(deviceType, segments);
174 final ThingUID thingUid = new ThingUID(thingTypeUid + AbstractUID.SEPARATOR + macAddress.replace(".", ""));
176 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUid) //
177 .withThingType(thingTypeUid) //
178 .withProperty("hostname", ip) // AnelConfiguration.hostname
179 .withProperty("user", USER) // AnelConfiguration.user
180 .withProperty("password", PASSWORD) // AnelConfiguration.password
181 .withProperty("udpSendPort", sendPort) // AnelConfiguration.udpSendPort
182 .withProperty("udpReceivePort", receivePort) // AnelConfiguration.udbReceivePort
183 .withProperty(IAnelConstants.UNIQUE_PROPERTY_NAME, macAddress) //
185 .withRepresentationProperty(IAnelConstants.UNIQUE_PROPERTY_NAME) //
188 thingDiscovered(discoveryResult);
192 private ThingTypeUID getThingTypeUid(@Nullable String deviceType, String[] segments) {
193 // device type is contained since firmware 6.0
194 if (deviceType != null && !deviceType.isEmpty()) {
195 final char deviceTypeChar = deviceType.charAt(0);
196 final ThingTypeUID thingTypeUID = IAnelConstants.DEVICE_TYPE_TO_THING_TYPE.get(deviceTypeChar);
197 if (thingTypeUID != null) {
202 if (segments.length < 20) {
203 // no information given, we should be save with return the simple firmware thing type
204 return IAnelConstants.THING_TYPE_ANEL_SIMPLE;
206 // more than 20 segments must include IO ports, hence it's an advanced firmware
207 return IAnelConstants.THING_TYPE_ANEL_ADVANCED;