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