]> git.basschouten.com Git - openhab-addons.git/blob
10289e09549bac93cccd13c54273204324449614
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.discovery;
14
15 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.THING_TYPE_GATEWAY;
16 import static org.openhab.binding.fineoffsetweatherstation.internal.Utils.toUInt16;
17
18 import java.io.IOException;
19 import java.net.DatagramPacket;
20 import java.net.DatagramSocket;
21 import java.net.InetAddress;
22 import java.net.InetSocketAddress;
23 import java.net.SocketException;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetGatewayConfiguration;
35 import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetSensorConfiguration;
36 import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants;
37 import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
38 import org.openhab.binding.fineoffsetweatherstation.internal.domain.Command;
39 import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice;
40 import org.openhab.core.config.discovery.AbstractDiscoveryService;
41 import org.openhab.core.config.discovery.DiscoveryResult;
42 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
43 import org.openhab.core.config.discovery.DiscoveryService;
44 import org.openhab.core.i18n.LocaleProvider;
45 import org.openhab.core.i18n.TranslationProvider;
46 import org.openhab.core.net.NetUtil;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingUID;
49 import org.osgi.framework.Bundle;
50 import org.osgi.framework.FrameworkUtil;
51 import org.osgi.service.component.annotations.Activate;
52 import org.osgi.service.component.annotations.Component;
53 import org.osgi.service.component.annotations.Reference;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 /**
58  * @author Andreas Berger - Initial contribution
59  */
60 @NonNullByDefault
61 @Component(service = { DiscoveryService.class, FineOffsetGatewayDiscoveryService.class }, immediate = true)
62 public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService {
63     public static final int DISCOVERY_PORT = 46000;
64     private static final int BUFFER_LENGTH = 255;
65
66     private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class);
67
68     private static final long REFRESH_INTERVAL = 600;
69     private static final int DISCOVERY_TIME = 5;
70     private final TranslationProvider translationProvider;
71     private final LocaleProvider localeProvider;
72     private final @Nullable Bundle bundle;
73     private @Nullable DatagramSocket clientSocket;
74     private @Nullable Thread socketReceiveThread;
75     private @Nullable ScheduledFuture<?> discoveryJob;
76
77     @Activate
78     public FineOffsetGatewayDiscoveryService(@Reference TranslationProvider translationProvider,
79             @Reference LocaleProvider localeProvider) throws IllegalArgumentException {
80         super(Collections.singleton(THING_TYPE_GATEWAY), DISCOVERY_TIME, true);
81         this.translationProvider = translationProvider;
82         this.localeProvider = localeProvider;
83         this.bundle = FrameworkUtil.getBundle(FineOffsetGatewayDiscoveryService.class);
84     }
85
86     @Override
87     protected void startBackgroundDiscovery() {
88         final @Nullable ScheduledFuture<?> discoveryJob = this.discoveryJob;
89         if (discoveryJob == null || discoveryJob.isCancelled()) {
90             this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discover, 0, REFRESH_INTERVAL, TimeUnit.SECONDS);
91         }
92     }
93
94     @Override
95     protected void stopBackgroundDiscovery() {
96         final @Nullable ScheduledFuture<?> discoveryJob = this.discoveryJob;
97         if (discoveryJob != null) {
98             discoveryJob.cancel(true);
99             this.discoveryJob = null;
100         }
101     }
102
103     @Override
104     public void deactivate() {
105         stopReceiverThreat();
106         final DatagramSocket clientSocket = this.clientSocket;
107         if (clientSocket != null) {
108             clientSocket.close();
109         }
110         this.clientSocket = null;
111         super.deactivate();
112     }
113
114     @Override
115     protected void startScan() {
116         final DatagramSocket clientSocket = getSocket();
117         if (clientSocket != null) {
118             logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
119             discover();
120         } else {
121             logger.debug("Discovery not started. Client DatagramSocket null");
122         }
123     }
124
125     private void discover() {
126         startReceiverThread();
127         NetUtil.getAllBroadcastAddresses().forEach(this::sendDiscoveryRequest);
128     }
129
130     public void addSensors(ThingUID bridgeUID, Collection<SensorDevice> sensorDevices) {
131         for (SensorDevice sensorDevice : sensorDevices) {
132             ThingUID uid = new ThingUID(FineOffsetWeatherStationBindingConstants.THING_TYPE_SENSOR, bridgeUID,
133                     sensorDevice.getSensorGatewayBinding().name());
134
135             String model = sensorDevice.getSensorGatewayBinding().getSensor().name();
136             String prefix = "thing.sensor." + model;
137             @Nullable
138             String name = translationProvider.getText(bundle, prefix + ".label", model, localeProvider.getLocale());
139             DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
140                     .withProperty(FineOffsetSensorConfiguration.SENSOR, sensorDevice.getSensorGatewayBinding().name())
141                     .withProperty(Thing.PROPERTY_MODEL_ID, model)
142                     .withRepresentationProperty(FineOffsetSensorConfiguration.SENSOR);
143
144             @Nullable
145             Integer channel = sensorDevice.getSensorGatewayBinding().getChannel();
146             if (channel != null) {
147                 builder.withProperty("channel", channel);
148                 name += " " + translationProvider.getText(bundle, "channel", "channel", localeProvider.getLocale())
149                         + " " + channel;
150             }
151             builder.withLabel(name);
152             @Nullable
153             String description = translationProvider.getText(bundle, prefix + ".description", model,
154                     localeProvider.getLocale());
155             if (description != null) {
156                 builder.withProperty("description", description);
157             }
158
159             DiscoveryResult result = builder.build();
160             thingDiscovered(result);
161         }
162     }
163
164     private void discovered(String ip, int port, byte[] macAddr, String name) {
165         String id = String.valueOf(Utils.toUInt64(macAddr, 0));
166
167         Map<String, Object> properties = new HashMap<>();
168         properties.put(Thing.PROPERTY_MAC_ADDRESS, Utils.toHexString(macAddr, macAddr.length, ":"));
169         properties.put(FineOffsetGatewayConfiguration.IP, ip);
170         properties.put(FineOffsetGatewayConfiguration.PORT, port);
171
172         ThingUID uid = new ThingUID(THING_TYPE_GATEWAY, id);
173         DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
174                 .withLabel(translationProvider.getText(bundle, "thing.gateway.label", name, localeProvider.getLocale()))
175                 .build();
176         thingDiscovered(result);
177         logger.debug("Thing discovered '{}'", result);
178     }
179
180     synchronized @Nullable DatagramSocket getSocket() {
181         DatagramSocket clientSocket = this.clientSocket;
182         if (clientSocket != null && clientSocket.isBound()) {
183             return clientSocket;
184         }
185         try {
186             logger.debug("Getting new socket for discovery");
187             clientSocket = new DatagramSocket();
188             clientSocket.setReuseAddress(true);
189             clientSocket.setBroadcast(true);
190             this.clientSocket = clientSocket;
191             return clientSocket;
192         } catch (SocketException | SecurityException e) {
193             logger.debug("Error getting socket for discovery: {}", e.getMessage());
194         }
195         return null;
196     }
197
198     private void closeSocket() {
199         final @Nullable DatagramSocket clientSocket = this.clientSocket;
200         if (clientSocket != null) {
201             clientSocket.close();
202         } else {
203             return;
204         }
205         this.clientSocket = null;
206     }
207
208     private void sendDiscoveryRequest(String broadcastAddress) {
209         final @Nullable DatagramSocket socket = getSocket();
210         if (socket != null) {
211             byte[] requestMessage = Command.CMD_BROADCAST.getPayload();
212             InetSocketAddress addr = new InetSocketAddress(broadcastAddress, DISCOVERY_PORT);
213             DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, addr);
214             try {
215                 socket.send(datagramPacket);
216             } catch (IOException e) {
217                 logger.trace("Discovery on {} error: {}", broadcastAddress, e.getMessage());
218             }
219         }
220     }
221
222     /**
223      * starts the {@link ReceiverThread} thread
224      */
225     private synchronized void startReceiverThread() {
226         final Thread srt = socketReceiveThread;
227         if (srt != null) {
228             if (srt.isAlive() && !srt.isInterrupted()) {
229                 return;
230             }
231         }
232         stopReceiverThreat();
233         Thread socketReceiveThread = new ReceiverThread();
234         socketReceiveThread.start();
235         this.socketReceiveThread = socketReceiveThread;
236     }
237
238     /**
239      * Stops the {@link ReceiverThread} thread
240      */
241     private synchronized void stopReceiverThreat() {
242         final Thread socketReceiveThread = this.socketReceiveThread;
243         if (socketReceiveThread != null) {
244             socketReceiveThread.interrupt();
245             this.socketReceiveThread = null;
246         }
247         closeSocket();
248     }
249
250     /**
251      * The thread, which waits for data and submits the unique results addresses to the discovery results
252      */
253     private class ReceiverThread extends Thread {
254         @Override
255         public void run() {
256             DatagramSocket socket = getSocket();
257             if (socket != null) {
258                 logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
259                 receiveData(socket);
260             }
261         }
262
263         /**
264          * This method waits for data and submits the unique results addresses to the discovery results
265          *
266          * @param socket - The multicast socket to (re)use
267          */
268         private void receiveData(DatagramSocket socket) {
269             DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
270             try {
271                 while (!interrupted()) {
272                     logger.trace("Thread {} waiting for data on port {}", this, socket.getLocalPort());
273                     socket.receive(receivePacket);
274                     String hostAddress = receivePacket.getAddress().getHostAddress();
275                     logger.trace("Received {} bytes response from {}:{} on Port {}", receivePacket.getLength(),
276                             hostAddress, receivePacket.getPort(), socket.getLocalPort());
277
278                     byte[] messageBuf = Arrays.copyOfRange(receivePacket.getData(), receivePacket.getOffset(),
279                             receivePacket.getOffset() + receivePacket.getLength());
280                     if (logger.isTraceEnabled()) {
281                         logger.trace("Discovery response received: {}",
282                                 Utils.toHexString(messageBuf, messageBuf.length, ""));
283                     }
284
285                     if (Command.CMD_BROADCAST.isHeaderValid(messageBuf)) {
286                         String ip = InetAddress.getByAddress(Arrays.copyOfRange(messageBuf, 11, 15)).getHostAddress();
287                         var macAddr = Arrays.copyOfRange(messageBuf, 5, 5 + 6);
288                         var port = toUInt16(messageBuf, 15);
289                         var len = Utils.toUInt8(messageBuf[17]);
290                         String name = new String(messageBuf, 18, len);
291                         scheduler.schedule(() -> {
292                             try {
293                                 discovered(ip, port, macAddr, name);
294                             } catch (Exception e) {
295                                 logger.debug("Error submitting discovered device at {}", ip, e);
296                             }
297                         }, 0, TimeUnit.SECONDS);
298                     }
299                 }
300             } catch (SocketException e) {
301                 logger.debug("Receiver thread received SocketException: {}", e.getMessage());
302             } catch (IOException e) {
303                 logger.trace("Receiver thread was interrupted");
304             }
305             logger.debug("Receiver thread ended");
306         }
307     }
308 }