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