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.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;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
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;
65 * @author Andreas Berger - Initial contribution
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;
73 private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class);
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;
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);
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);
102 protected void stopBackgroundDiscovery() {
103 final @Nullable ScheduledFuture<?> discoveryJob = this.discoveryJob;
104 if (discoveryJob != null) {
105 discoveryJob.cancel(true);
106 this.discoveryJob = null;
111 public void deactivate() {
112 stopReceiverThreat();
113 final DatagramSocket clientSocket = this.clientSocket;
114 if (clientSocket != null) {
115 clientSocket.close();
117 this.clientSocket = null;
122 protected void startScan() {
123 final DatagramSocket clientSocket = getSocket();
124 if (clientSocket != null) {
125 logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
128 logger.debug("Discovery not started. Client DatagramSocket null");
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,
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());
146 String model = sensorDevice.getSensorGatewayBinding().getSensor().name();
147 String prefix = "thing.sensor." + model;
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);
156 Integer channel = sensorDevice.getSensorGatewayBinding().getChannel();
157 if (channel != null) {
158 builder.withProperty("channel", channel);
159 name += " " + translationProvider.getText(bundle, "channel", "channel", localeProvider.getLocale())
162 builder.withLabel(name);
164 String description = translationProvider.getText(bundle, prefix + ".description", model,
165 localeProvider.getLocale());
166 if (description != null) {
167 builder.withProperty("description", description);
170 DiscoveryResult result = builder.build();
171 thingDiscovered(result);
175 private void discovered(String ip, int port, byte[] macAddr, String name) {
176 String id = String.valueOf(Utils.toUInt64(macAddr, 0));
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());
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()))
193 thingDiscovered(result);
194 logger.debug("Thing discovered '{}'", result);
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()) {
208 } catch (IOException e) {
215 synchronized @Nullable DatagramSocket getSocket() {
216 DatagramSocket clientSocket = this.clientSocket;
217 if (clientSocket != null && clientSocket.isBound()) {
221 logger.debug("Getting new socket for discovery");
222 clientSocket = new DatagramSocket();
223 clientSocket.setReuseAddress(true);
224 clientSocket.setBroadcast(true);
225 this.clientSocket = clientSocket;
227 } catch (SocketException | SecurityException e) {
228 logger.debug("Error getting socket for discovery: {}", e.getMessage());
233 private void closeSocket() {
234 final @Nullable DatagramSocket clientSocket = this.clientSocket;
235 if (clientSocket != null) {
236 clientSocket.close();
240 this.clientSocket = null;
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, ""));
251 socket.send(datagramPacket);
252 } catch (IOException e) {
253 logger.trace("Discovery on {} error: {}", broadcastAddress, e.getMessage());
259 * starts the {@link ReceiverThread} thread
261 private synchronized void startReceiverThread() {
262 final Thread srt = socketReceiveThread;
264 if (srt.isAlive() && !srt.isInterrupted()) {
268 stopReceiverThreat();
269 Thread socketReceiveThread = new ReceiverThread();
270 socketReceiveThread.start();
271 this.socketReceiveThread = socketReceiveThread;
275 * Stops the {@link ReceiverThread} thread
277 private synchronized void stopReceiverThreat() {
278 final Thread socketReceiveThread = this.socketReceiveThread;
279 if (socketReceiveThread != null) {
280 socketReceiveThread.interrupt();
281 this.socketReceiveThread = null;
287 * The thread, which waits for data and submits the unique results addresses to the discovery results
289 private class ReceiverThread extends Thread {
292 DatagramSocket socket = getSocket();
293 if (socket != null) {
294 logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
300 * This method waits for data and submits the unique results addresses to the discovery results
302 * @param socket - The multicast socket to (re)use
304 private void receiveData(DatagramSocket socket) {
305 DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
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());
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, ""));
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(() -> {
329 discovered(ip, port, macAddr, name);
330 } catch (Exception e) {
331 logger.debug("Error submitting discovered device at {}", ip, e);
333 }, 0, TimeUnit.SECONDS);
336 } catch (SocketException e) {
337 logger.debug("Receiver thread received SocketException: {}", e.getMessage());
338 } catch (IOException e) {
339 logger.trace("Receiver thread was interrupted");
341 logger.debug("Receiver thread ended");