]> git.basschouten.com Git - openhab-addons.git/blob
41caa630318e07285774dae40e41f69313ba6ed9
[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.anel.internal.discovery;
14
15 import java.io.IOException;
16 import java.net.BindException;
17 import java.nio.channels.ClosedByInterruptException;
18 import java.util.Set;
19 import java.util.TreeSet;
20
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;
37
38 /**
39  * Discovery service for ANEL devices.
40  *
41  * @author Patrick Koenemann - Initial contribution
42  */
43 @NonNullByDefault
44 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.anel")
45 public class AnelDiscoveryService extends AbstractDiscoveryService {
46
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());
51
52     private static final int DISCOVER_DEVICE_TIMEOUT_SECONDS = 2;
53
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);
57
58     private final Logger logger = LoggerFactory.getLogger(AnelDiscoveryService.class);
59
60     private @Nullable Thread scanningThread = null;
61
62     public AnelDiscoveryService() throws IllegalArgumentException {
63         super(IAnelConstants.SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS);
64         logger.debug(
65                 "Anel NET-PwrCtrl discovery service instantiated for broadcast addresses {} with a timeout of {} seconds.",
66                 BROADCAST_ADDRESSES, DISCOVER_TIMEOUT_SECONDS);
67     }
68
69     @Override
70     protected void startScan() {
71         /*
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.
75          */
76         final Thread thread = new NamedThreadFactory(IAnelConstants.BINDING_ID, true).newThread(this::doScan);
77         thread.start();
78         scanningThread = thread;
79     }
80
81     private void doScan() {
82         logger.debug("Starting scan of Anel devices via UDP broadcast messages...");
83
84         try {
85             for (final String broadcastAddress : BROADCAST_ADDRESSES) {
86
87                 // for each available broadcast network address try factory default ports first
88                 scan(broadcastAddress, IAnelConstants.DEFAULT_SEND_PORT, IAnelConstants.DEFAULT_RECEIVE_PORT);
89
90                 // try reasonable ports...
91                 for (int[] ports : DISCOVERY_PORTS) {
92                     int sendPort = ports[0];
93                     int receivePort = ports[1];
94
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]) {
97                         sendPort++;
98                         receivePort++;
99                     }
100                 }
101             }
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);
106         } finally {
107             scanningThread = null;
108         }
109         logger.debug("Scan finished.");
110     }
111
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);
117
118         try {
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
127                         } else {
128                             deviceDiscovered[0] = true; // we discover the device!
129                         }
130                     }
131                     if (discoverDevice) {
132                         // discover device outside synchronized-block
133                         deviceDiscovered(status, sendPort, receivePort);
134                     }
135                 }
136             }, false);
137
138             udpConnector.send(IAnelConstants.BROADCAST_DISCOVERY_MSG);
139
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
143             }
144
145             return deviceDiscovered[0];
146         } catch (BindException e) {
147             // most likely socket is already in use, ignore this exception.
148             logger.debug(
149                     "Invalid address {} or one of the ports {} or {} is already in use. Skipping scan of these ports.",
150                     broadcastAddress, sendPort, receivePort);
151         } finally {
152             udpConnector.disconnect();
153         }
154         return false;
155     }
156
157     @Override
158     protected synchronized void stopScan() {
159         final Thread thread = scanningThread;
160         if (thread != null) {
161             thread.interrupt();
162         }
163         super.stopScan();
164     }
165
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(".", ""));
175
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) //
184                     .withLabel(name) //
185                     .withRepresentationProperty(IAnelConstants.UNIQUE_PROPERTY_NAME) //
186                     .build();
187
188             thingDiscovered(discoveryResult);
189         }
190     }
191
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) {
198                 return thingTypeUID;
199             }
200         }
201
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;
205         } else {
206             // more than 20 segments must include IO ports, hence it's an advanced firmware
207             return IAnelConstants.THING_TYPE_ANEL_ADVANCED;
208         }
209     }
210 }