2 * Copyright (c) 2010-2023 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;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
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;
64 * @author Andreas Berger - Initial contribution
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;
72 private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class);
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;
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);
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);
101 protected void stopBackgroundDiscovery() {
102 final @Nullable ScheduledFuture<?> discoveryJob = this.discoveryJob;
103 if (discoveryJob != null) {
104 discoveryJob.cancel(true);
105 this.discoveryJob = null;
110 public void deactivate() {
111 stopReceiverThreat();
112 final DatagramSocket clientSocket = this.clientSocket;
113 if (clientSocket != null) {
114 clientSocket.close();
116 this.clientSocket = null;
121 protected void startScan() {
122 final DatagramSocket clientSocket = getSocket();
123 if (clientSocket != null) {
124 logger.debug("Discovery using socket on port {}", clientSocket.getLocalPort());
127 logger.debug("Discovery not started. Client DatagramSocket null");
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,
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());
145 String model = sensorDevice.getSensorGatewayBinding().getSensor().name();
146 String prefix = "thing.sensor." + model;
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);
155 Integer channel = sensorDevice.getSensorGatewayBinding().getChannel();
156 if (channel != null) {
157 builder.withProperty("channel", channel);
158 name += " " + translationProvider.getText(bundle, "channel", "channel", localeProvider.getLocale())
161 builder.withLabel(name);
163 String description = translationProvider.getText(bundle, prefix + ".description", model,
164 localeProvider.getLocale());
165 if (description != null) {
166 builder.withProperty("description", description);
169 DiscoveryResult result = builder.build();
170 thingDiscovered(result);
174 private void discovered(String ip, int port, byte[] macAddr, String name) {
175 String id = String.valueOf(Utils.toUInt64(macAddr, 0));
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());
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()))
192 thingDiscovered(result);
193 logger.debug("Thing discovered '{}'", result);
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()) {
207 } catch (IOException e) {
214 synchronized @Nullable DatagramSocket getSocket() {
215 DatagramSocket clientSocket = this.clientSocket;
216 if (clientSocket != null && clientSocket.isBound()) {
220 logger.debug("Getting new socket for discovery");
221 clientSocket = new DatagramSocket();
222 clientSocket.setReuseAddress(true);
223 clientSocket.setBroadcast(true);
224 this.clientSocket = clientSocket;
226 } catch (SocketException | SecurityException e) {
227 logger.debug("Error getting socket for discovery: {}", e.getMessage());
232 private void closeSocket() {
233 final @Nullable DatagramSocket clientSocket = this.clientSocket;
234 if (clientSocket != null) {
235 clientSocket.close();
239 this.clientSocket = null;
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, ""));
250 socket.send(datagramPacket);
251 } catch (IOException e) {
252 logger.trace("Discovery on {} error: {}", broadcastAddress, e.getMessage());
258 * starts the {@link ReceiverThread} thread
260 private synchronized void startReceiverThread() {
261 final Thread srt = socketReceiveThread;
263 if (srt.isAlive() && !srt.isInterrupted()) {
267 stopReceiverThreat();
268 Thread socketReceiveThread = new ReceiverThread();
269 socketReceiveThread.start();
270 this.socketReceiveThread = socketReceiveThread;
274 * Stops the {@link ReceiverThread} thread
276 private synchronized void stopReceiverThreat() {
277 final Thread socketReceiveThread = this.socketReceiveThread;
278 if (socketReceiveThread != null) {
279 socketReceiveThread.interrupt();
280 this.socketReceiveThread = null;
286 * The thread, which waits for data and submits the unique results addresses to the discovery results
288 private class ReceiverThread extends Thread {
291 DatagramSocket socket = getSocket();
292 if (socket != null) {
293 logger.debug("Starting discovery receiver thread for socket on port {}", socket.getLocalPort());
299 * This method waits for data and submits the unique results addresses to the discovery results
301 * @param socket - The multicast socket to (re)use
303 private void receiveData(DatagramSocket socket) {
304 DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
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());
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, ""));
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(() -> {
328 discovered(ip, port, macAddr, name);
329 } catch (Exception e) {
330 logger.debug("Error submitting discovered device at {}", ip, e);
332 }, 0, TimeUnit.SECONDS);
335 } catch (SocketException e) {
336 logger.debug("Receiver thread received SocketException: {}", e.getMessage());
337 } catch (IOException e) {
338 logger.trace("Receiver thread was interrupted");
340 logger.debug("Receiver thread ended");