2 * Copyright (c) 2010-2024 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.pilight.internal.discovery;
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.net.DatagramPacket;
18 import java.net.DatagramSocket;
19 import java.net.Inet4Address;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.NetworkInterface;
23 import java.net.SocketException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.Collections;
26 import java.util.Enumeration;
27 import java.util.HashMap;
28 import java.util.List;
30 import java.util.Scanner;
32 import java.util.concurrent.ScheduledFuture;
33 import java.util.concurrent.TimeUnit;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.pilight.internal.PilightBindingConstants;
38 import org.openhab.core.config.discovery.AbstractDiscoveryService;
39 import org.openhab.core.config.discovery.DiscoveryResult;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.DiscoveryService;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.thing.ThingUID;
44 import org.osgi.service.component.annotations.Component;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link PilightBridgeDiscoveryService} is responsible for discovering new pilight daemons on the network
50 * by sending a ssdp multicast request via udp.
52 * @author Niklas Dörfler - Initial contribution
55 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.pilight")
56 public class PilightBridgeDiscoveryService extends AbstractDiscoveryService {
58 private static final int AUTODISCOVERY_SEARCH_TIME_SEC = 5;
59 private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC = 10 * 60; // 10 minutes
61 private static final String SSDP_DISCOVERY_REQUEST_MESSAGE = """
63 Host:239.255.255.250:1900
64 ST:urn:schemas-upnp-org:service:pilight:1
70 public static final String SSDP_MULTICAST_ADDRESS = "239.255.255.250";
71 public static final int SSDP_PORT = 1900;
72 public static final int SSDP_WAIT_TIMEOUT = 2000; // in milliseconds
74 private final Logger logger = LoggerFactory.getLogger(PilightBridgeDiscoveryService.class);
76 private @Nullable ScheduledFuture<?> backgroundDiscoveryJob;
78 public PilightBridgeDiscoveryService() throws IllegalArgumentException {
79 super(getSupportedThingTypeUIDs(), AUTODISCOVERY_SEARCH_TIME_SEC, true);
82 public static Set<ThingTypeUID> getSupportedThingTypeUIDs() {
83 return Set.of(PilightBindingConstants.THING_TYPE_BRIDGE);
87 protected void startScan() {
88 logger.debug("Pilight bridge discovery scan started");
89 removeOlderResults(getTimestampOfLastScan());
91 List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
92 for (NetworkInterface nic : interfaces) {
93 Enumeration<InetAddress> inetAddresses = nic.getInetAddresses();
94 for (InetAddress inetAddress : Collections.list(inetAddresses)) {
95 if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
97 DatagramSocket ssdp = new DatagramSocket(
98 new InetSocketAddress(inetAddress.getHostAddress(), 0));
99 byte[] buff = SSDP_DISCOVERY_REQUEST_MESSAGE.getBytes(StandardCharsets.UTF_8);
100 DatagramPacket sendPack = new DatagramPacket(buff, buff.length);
101 sendPack.setAddress(InetAddress.getByName(SSDP_MULTICAST_ADDRESS));
102 sendPack.setPort(SSDP_PORT);
104 ssdp.setSoTimeout(SSDP_WAIT_TIMEOUT);
108 DatagramPacket recvPack = new DatagramPacket(new byte[1024], 1024);
109 ssdp.receive(recvPack);
110 byte[] recvData = recvPack.getData();
112 final Scanner scanner = new Scanner(new ByteArrayInputStream(recvData),
113 StandardCharsets.UTF_8);
114 loop = scanner.findAll("Location:([0-9.]+):(.*)").peek(matchResult -> {
115 final String server = matchResult.group(1);
116 final Integer port = Integer.parseInt(matchResult.group(2));
117 final String bridgeName = server.replace(".", "") + "" + port;
119 logger.debug("Found pilight daemon at {}:{}", server, port);
121 Map<String, Object> properties = new HashMap<>();
122 properties.put(PilightBindingConstants.PROPERTY_IP_ADDRESS, server);
123 properties.put(PilightBindingConstants.PROPERTY_PORT, port);
124 properties.put(PilightBindingConstants.PROPERTY_NAME, bridgeName);
126 ThingUID uid = new ThingUID(PilightBindingConstants.THING_TYPE_BRIDGE, bridgeName);
128 DiscoveryResult result = DiscoveryResultBuilder.create(uid)
129 .withProperties(properties)
130 .withRepresentationProperty(PilightBindingConstants.PROPERTY_NAME)
131 .withLabel("Pilight Bridge (" + server + ")").build();
133 thingDiscovered(result);
136 } catch (IOException e) {
142 } catch (SocketException e) {
143 logger.warn("Unable to enumerate the local network interfaces", e);
148 protected synchronized void stopScan() {
150 removeOlderResults(getTimestampOfLastScan());
154 protected void startBackgroundDiscovery() {
155 logger.debug("Start Pilight device background discovery");
156 final @Nullable ScheduledFuture<?> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
157 if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) {
158 this.backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 5,
159 AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC, TimeUnit.SECONDS);
164 protected void stopBackgroundDiscovery() {
165 logger.debug("Stop Pilight device background discovery");
166 final @Nullable ScheduledFuture<?> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
167 if (backgroundDiscoveryJob != null) {
168 backgroundDiscoveryJob.cancel(true);
169 this.backgroundDiscoveryJob = null;