]> git.basschouten.com Git - openhab-addons.git/blob
6415fe566590595fc8ad29181960803f5af42931
[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.unifiedremote.internal;
14
15 import static org.openhab.binding.unifiedremote.internal.UnifiedRemoteBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.net.SocketException;
22 import java.net.SocketTimeoutException;
23 import java.text.ParseException;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.concurrent.TimeUnit;
28 import java.util.function.Consumer;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.openhab.core.config.discovery.AbstractDiscoveryService;
32 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
33 import org.openhab.core.config.discovery.DiscoveryService;
34 import org.openhab.core.thing.ThingUID;
35 import org.osgi.service.component.annotations.Component;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link UnifiedRemoteDiscoveryService} discover Unified Remote Server Instances in the network.
41  *
42  * @author Miguel Alvarez - Initial contribution
43  */
44 @Component(service = DiscoveryService.class, configurationPid = "discovery.unifiedremote")
45 @NonNullByDefault
46 public class UnifiedRemoteDiscoveryService extends AbstractDiscoveryService {
47
48     private Logger logger = LoggerFactory.getLogger(UnifiedRemoteDiscoveryService.class);
49     static final int TIMEOUT_MS = 20000;
50     private static final long DISCOVERY_RESULT_TTL_SEC = TimeUnit.MINUTES.toSeconds(5);
51
52     /**
53      * Port used for broadcast and listening.
54      */
55     public static final int DISCOVERY_PORT = 9511;
56     /**
57      * String the client sends, to disambiguate packets on this port.
58      */
59     public static final String DISCOVERY_REQUEST = "6N T|-Ar-A6N T|-Ar-A6N T|-Ar-A";
60     /**
61      * String the client sends, to disambiguate packets on this port.
62      */
63     public static final String DISCOVERY_RESPONSE_PREFIX = ")-b@ h): :)i)-b@ h): :)i)-b@ h): :)";
64     /**
65      * String used to replace non printable characters on service response
66      */
67     public static final String NON_PRINTABLE_CHARTS_REPLACEMENT = ": :";
68
69     private static final int MAX_PACKET_SIZE = 2048;
70     /**
71      * maximum time to wait for a reply, in milliseconds.
72      */
73     private static final int SOCKET_TIMEOUT_MS = 3000;
74
75     public UnifiedRemoteDiscoveryService() {
76         super(SUPPORTED_THING_TYPES, TIMEOUT_MS, false);
77     }
78
79     @Override
80     protected void startScan() {
81         sendBroadcast(this::addNewServer);
82     }
83
84     private void addNewServer(ServerInfo serverInfo) {
85         Map<String, Object> properties = new HashMap<>();
86         properties.put(PARAMETER_MAC_ADDRESS, serverInfo.macAddress);
87         properties.put(PARAMETER_HOSTNAME, serverInfo.host);
88         properties.put(PARAMETER_TCP_PORT, serverInfo.tcpPort);
89         properties.put(PARAMETER_UDP_PORT, serverInfo.udpPort);
90         thingDiscovered(
91                 DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_UNIFIED_REMOTE_SERVER, serverInfo.macAddress))
92                         .withTTL(DISCOVERY_RESULT_TTL_SEC).withRepresentationProperty(PARAMETER_MAC_ADDRESS)
93                         .withProperties(properties).withLabel(serverInfo.name).build());
94     }
95
96     /**
97      * Create a UDP socket on the service discovery broadcast port.
98      *
99      * @return open DatagramSocket if successful
100      * @throws RuntimeException if cannot create the socket
101      */
102     public DatagramSocket createSocket() throws SocketException {
103         DatagramSocket socket;
104         socket = new DatagramSocket();
105         socket.setBroadcast(true);
106         socket.setSoTimeout(TIMEOUT_MS);
107         return socket;
108     }
109
110     private ServerInfo tryParseServerDiscovery(DatagramPacket receivePacket) throws ParseException {
111         String host = receivePacket.getAddress().getHostAddress();
112         String reply = new String(receivePacket.getData()).replaceAll("[\\p{C}]", NON_PRINTABLE_CHARTS_REPLACEMENT)
113                 .replaceAll("[^\\x00-\\x7F]", NON_PRINTABLE_CHARTS_REPLACEMENT);
114         if (!reply.startsWith(DISCOVERY_RESPONSE_PREFIX)) {
115             throw new ParseException("Bad discovery response prefix", 0);
116         }
117         String[] parts = Arrays
118                 .stream(reply.replace(DISCOVERY_RESPONSE_PREFIX, "").split(NON_PRINTABLE_CHARTS_REPLACEMENT))
119                 .filter((String e) -> e.length() != 0).toArray(String[]::new);
120         String name = parts[0];
121         int tcpPort = Integer.parseInt(parts[1]);
122         int udpPort = Integer.parseInt(parts[3]);
123         String macAddress = parts[2];
124         return new ServerInfo(host, tcpPort, udpPort, name, macAddress);
125     }
126
127     /**
128      * Send broadcast packets with service request string until a response
129      * is received. Return the response as String (even though it should
130      * contain an internet address).
131      *
132      * @return String received from server. Should be server IP address.
133      *         Returns empty string if failed to get valid reply.
134      */
135     public void sendBroadcast(Consumer<ServerInfo> listener) {
136         byte[] receiveBuffer = new byte[MAX_PACKET_SIZE];
137         DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
138
139         DatagramSocket socket = null;
140         try {
141             socket = createSocket();
142         } catch (SocketException e) {
143             logger.debug("Error creating discovery socket: {}", e.getMessage());
144             return;
145         }
146         byte[] packetData = DISCOVERY_REQUEST.getBytes();
147         try {
148             InetAddress broadcastAddress = InetAddress.getByName("255.255.255.255");
149             int servicePort = DISCOVERY_PORT;
150             DatagramPacket packet = new DatagramPacket(packetData, packetData.length, broadcastAddress, servicePort);
151             socket.send(packet);
152             logger.debug("Sent packet to {}:{}", broadcastAddress.getHostAddress(), servicePort);
153             for (int i = 0; i < 20; i++) {
154                 socket.receive(receivePacket);
155                 String host = receivePacket.getAddress().getHostAddress();
156                 logger.debug("Received reply from {}", host);
157                 try {
158                     ServerInfo serverInfo = tryParseServerDiscovery(receivePacket);
159                     listener.accept(serverInfo);
160                 } catch (ParseException ex) {
161                     logger.debug("Unable to parse server discovery response from {}: {}", host, ex.getMessage());
162                 }
163             }
164         } catch (SocketTimeoutException ste) {
165             logger.debug("SocketTimeoutException during socket operation: {}", ste.getMessage());
166         } catch (IOException ioe) {
167             logger.debug("IOException during socket operation: {}", ioe.getMessage());
168         } finally {
169             socket.close();
170         }
171     }
172
173     public class ServerInfo {
174         String name;
175         int tcpPort;
176         int udpPort;
177         String host;
178         String macAddress;
179
180         ServerInfo(String host, int tcpPort, int udpPort, String name, String macAddress) {
181             this.name = name;
182             this.tcpPort = tcpPort;
183             this.udpPort = udpPort;
184             this.host = host;
185             this.macAddress = macAddress;
186         }
187     }
188 }