]> git.basschouten.com Git - openhab-addons.git/blob
423c04cabb3d50121c6ae441655beb5ede717746
[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.pilight.internal.discovery;
14
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.nio.charset.StandardCharsets;
24 import java.util.Collections;
25 import java.util.Enumeration;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Scanner;
30 import java.util.Set;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.concurrent.TimeUnit;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.pilight.internal.PilightBindingConstants;
37 import org.openhab.core.config.discovery.AbstractDiscoveryService;
38 import org.openhab.core.config.discovery.DiscoveryResult;
39 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
40 import org.openhab.core.config.discovery.DiscoveryService;
41 import org.openhab.core.thing.ThingTypeUID;
42 import org.openhab.core.thing.ThingUID;
43 import org.osgi.service.component.annotations.Component;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * The {@link PilightBridgeDiscoveryService} is responsible for discovering new pilight daemons on the network
49  * by sending a ssdp multicast request via udp.
50  *
51  * @author Niklas Dörfler - Initial contribution
52  */
53 @NonNullByDefault
54 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.pilight")
55 public class PilightBridgeDiscoveryService extends AbstractDiscoveryService {
56
57     private static final int AUTODISCOVERY_SEARCH_TIME_SEC = 5;
58     private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC = 60 * 10;
59
60     private static final String SSDP_DISCOVERY_REQUEST_MESSAGE = """
61             M-SEARCH * HTTP/1.1
62             Host:239.255.255.250:1900
63             ST:urn:schemas-upnp-org:service:pilight:1
64             Man:"ssdp:discover"
65             MX:3
66
67             """;
68     public static final String SSDP_MULTICAST_ADDRESS = "239.255.255.250";
69     public static final int SSDP_PORT = 1900;
70     public static final int SSDP_WAIT_TIMEOUT = 2000; // in milliseconds
71
72     private final Logger logger = LoggerFactory.getLogger(PilightBridgeDiscoveryService.class);
73
74     private @Nullable ScheduledFuture<?> backgroundDiscoveryJob;
75
76     public PilightBridgeDiscoveryService() throws IllegalArgumentException {
77         super(getSupportedThingTypeUIDs(), AUTODISCOVERY_SEARCH_TIME_SEC, true);
78     }
79
80     public static Set<ThingTypeUID> getSupportedThingTypeUIDs() {
81         return Set.of(PilightBindingConstants.THING_TYPE_BRIDGE);
82     }
83
84     @Override
85     protected void startScan() {
86         logger.debug("Pilight bridge discovery scan started");
87         removeOlderResults(getTimestampOfLastScan());
88         try {
89             List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
90             for (NetworkInterface nic : interfaces) {
91                 Enumeration<InetAddress> inetAddresses = nic.getInetAddresses();
92                 for (InetAddress inetAddress : Collections.list(inetAddresses)) {
93                     if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
94                         DatagramSocket ssdp = new DatagramSocket(
95                                 new InetSocketAddress(inetAddress.getHostAddress(), 0));
96                         byte[] buff = SSDP_DISCOVERY_REQUEST_MESSAGE.getBytes(StandardCharsets.UTF_8);
97                         DatagramPacket sendPack = new DatagramPacket(buff, buff.length);
98                         sendPack.setAddress(InetAddress.getByName(SSDP_MULTICAST_ADDRESS));
99                         sendPack.setPort(SSDP_PORT);
100                         ssdp.send(sendPack);
101                         ssdp.setSoTimeout(SSDP_WAIT_TIMEOUT);
102
103                         boolean loop = true;
104                         while (loop) {
105                             DatagramPacket recvPack = new DatagramPacket(new byte[1024], 1024);
106                             ssdp.receive(recvPack);
107                             byte[] recvData = recvPack.getData();
108
109                             final Scanner scanner = new Scanner(new ByteArrayInputStream(recvData),
110                                     StandardCharsets.UTF_8);
111                             loop = scanner.findAll("Location:([0-9.]+):(.*)").peek(matchResult -> {
112                                 final String server = matchResult.group(1);
113                                 final Integer port = Integer.parseInt(matchResult.group(2));
114                                 final String bridgeName = server.replace(".", "") + "" + port;
115
116                                 logger.debug("Found pilight daemon at {}:{}", server, port);
117
118                                 Map<String, Object> properties = new HashMap<>();
119                                 properties.put(PilightBindingConstants.PROPERTY_IP_ADDRESS, server);
120                                 properties.put(PilightBindingConstants.PROPERTY_PORT, port);
121                                 properties.put(PilightBindingConstants.PROPERTY_NAME, bridgeName);
122
123                                 ThingUID uid = new ThingUID(PilightBindingConstants.THING_TYPE_BRIDGE, bridgeName);
124
125                                 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
126                                         .withRepresentationProperty(PilightBindingConstants.PROPERTY_NAME)
127                                         .withLabel("Pilight Bridge (" + server + ")").build();
128
129                                 thingDiscovered(result);
130                             }).count() == 0;
131                         }
132                     }
133                 }
134             }
135         } catch (IOException e) {
136             if (e.getMessage() != null && !"Receive timed out".equals(e.getMessage())) {
137                 logger.warn("Unable to enumerate the local network interfaces {}", e.getMessage());
138             }
139         }
140     }
141
142     @Override
143     protected synchronized void stopScan() {
144         super.stopScan();
145         removeOlderResults(getTimestampOfLastScan());
146     }
147
148     @Override
149     protected void startBackgroundDiscovery() {
150         logger.debug("Start Pilight device background discovery");
151         final @Nullable ScheduledFuture<?> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
152         if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) {
153             this.backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 5,
154                     AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC, TimeUnit.SECONDS);
155         }
156     }
157
158     @Override
159     protected void stopBackgroundDiscovery() {
160         logger.debug("Stop Pilight device background discovery");
161         final @Nullable ScheduledFuture<?> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
162         if (backgroundDiscoveryJob != null) {
163             backgroundDiscoveryJob.cancel(true);
164             this.backgroundDiscoveryJob = null;
165         }
166     }
167 }