2 * Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.discovery;
15 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.THING_TYPE_GATEWAY;
16 import static org.openhab.binding.fineoffsetweatherstation.internal.Utils.toUInt16;
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;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
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;
58 * @author Andreas Berger - Initial contribution
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;
66 private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class);
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;
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);
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);
95 protected void stopBackgroundDiscovery() {
96 final @Nullable ScheduledFuture<?> discoveryJob = this.discoveryJob;
97 if (discoveryJob != null) {
98 discoveryJob.cancel(true);
99 this.discoveryJob = null;
104 public void deactivate() {
105 stopReceiverThreat();
106 final DatagramSocket clientSocket = this.clientSocket;
107 if (clientSocket != null) {
108 clientSocket.close();
110 this.clientSocket = null;
115 protected void startScan() {
116 final DatagramSocket clientSocket = getSocket();
117 if (clientSocket != null) {
118 logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
121 logger.debug("Discovery not started. Client DatagramSocket null");
125 private void discover() {
126 startReceiverThread();
127 NetUtil.getAllBroadcastAddresses().forEach(this::sendDiscoveryRequest);
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());
135 String model = sensorDevice.getSensorGatewayBinding().getSensor().name();
136 String prefix = "thing.sensor." + model;
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);
145 Integer channel = sensorDevice.getSensorGatewayBinding().getChannel();
146 if (channel != null) {
147 builder.withProperty("channel", channel);
148 name += " " + translationProvider.getText(bundle, "channel", "channel", localeProvider.getLocale())
151 builder.withLabel(name);
153 String description = translationProvider.getText(bundle, prefix + ".description", model,
154 localeProvider.getLocale());
155 if (description != null) {
156 builder.withProperty("description", description);
159 DiscoveryResult result = builder.build();
160 thingDiscovered(result);
164 private void discovered(String ip, int port, byte[] macAddr, String name) {
165 String id = String.valueOf(Utils.toUInt64(macAddr, 0));
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);
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()))
176 thingDiscovered(result);
177 logger.debug("Thing discovered '{}'", result);
180 synchronized @Nullable DatagramSocket getSocket() {
181 DatagramSocket clientSocket = this.clientSocket;
182 if (clientSocket != null && clientSocket.isBound()) {
186 logger.debug("Getting new socket for discovery");
187 clientSocket = new DatagramSocket();
188 clientSocket.setReuseAddress(true);
189 clientSocket.setBroadcast(true);
190 this.clientSocket = clientSocket;
192 } catch (SocketException | SecurityException e) {
193 logger.debug("Error getting socket for discovery: {}", e.getMessage());
198 private void closeSocket() {
199 final @Nullable DatagramSocket clientSocket = this.clientSocket;
200 if (clientSocket != null) {
201 clientSocket.close();
205 this.clientSocket = null;
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);
215 socket.send(datagramPacket);
216 } catch (IOException e) {
217 logger.trace("Discovery on {} error: {}", broadcastAddress, e.getMessage());
223 * starts the {@link ReceiverThread} thread
225 private synchronized void startReceiverThread() {
226 final Thread srt = socketReceiveThread;
228 if (srt.isAlive() && !srt.isInterrupted()) {
232 stopReceiverThreat();
233 Thread socketReceiveThread = new ReceiverThread();
234 socketReceiveThread.start();
235 this.socketReceiveThread = socketReceiveThread;
239 * Stops the {@link ReceiverThread} thread
241 private synchronized void stopReceiverThreat() {
242 final Thread socketReceiveThread = this.socketReceiveThread;
243 if (socketReceiveThread != null) {
244 socketReceiveThread.interrupt();
245 this.socketReceiveThread = null;
251 * The thread, which waits for data and submits the unique results addresses to the discovery results
253 private class ReceiverThread extends Thread {
256 DatagramSocket socket = getSocket();
257 if (socket != null) {
258 logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
264 * This method waits for data and submits the unique results addresses to the discovery results
266 * @param socket - The multicast socket to (re)use
268 private void receiveData(DatagramSocket socket) {
269 DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
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());
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, ""));
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(() -> {
293 discovered(ip, port, macAddr, name);
294 } catch (Exception e) {
295 logger.debug("Error submitting discovered device at {}", ip, e);
297 }, 0, TimeUnit.SECONDS);
300 } catch (SocketException e) {
301 logger.debug("Receiver thread received SocketException: {}", e.getMessage());
302 } catch (IOException e) {
303 logger.trace("Receiver thread was interrupted");
305 logger.debug("Receiver thread ended");