]> git.basschouten.com Git - openhab-addons.git/blob
4a85b2ccd6022e336a53b570b25de928adc70e14
[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.benqprojector.internal.discovery;
14
15 import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.InetAddress;
20 import java.net.InetSocketAddress;
21 import java.net.InterfaceAddress;
22 import java.net.MulticastSocket;
23 import java.net.NetworkInterface;
24 import java.net.SocketException;
25 import java.net.SocketTimeoutException;
26 import java.nio.charset.StandardCharsets;
27 import java.util.Enumeration;
28 import java.util.HashMap;
29 import java.util.Locale;
30 import java.util.Map;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.core.thing.Thing;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * The {@link MulticastListener} class is responsible for listening for the BenQ projector device announcement
40  * beacons on the multicast address, and then extracting the data fields out of the received datagram.
41  *
42  * @author Mark Hilbush - Initial contribution
43  * @author Michael Lobstein - Adapted for the BenQ Projector binding
44  */
45 @NonNullByDefault
46 public class MulticastListener {
47     private final Logger logger = LoggerFactory.getLogger(MulticastListener.class);
48
49     private MulticastSocket socket;
50     private InetSocketAddress inetSocketAddress;
51
52     // BenQ projector devices announce themselves on the AMX DDD multicast port
53     private static final String AMX_MULTICAST_GROUP = "239.255.250.250";
54     private static final int AMX_MULTICAST_PORT = 9131;
55
56     // How long to wait in milliseconds for a discovery beacon
57     public static final int DEFAULT_SOCKET_TIMEOUT_SEC = 3000;
58
59     /*
60      * Constructor joins the multicast group, throws IOException on failure.
61      */
62     public MulticastListener(String ipv4Address) throws IOException, SocketException {
63         InetAddress ifAddress = InetAddress.getByName(ipv4Address);
64         NetworkInterface networkInterface = getMulticastInterface(ipv4Address);
65         logger.debug("Discovery job using address {} on network interface {}", ifAddress.getHostAddress(),
66                 networkInterface.getName());
67         socket = new MulticastSocket(AMX_MULTICAST_PORT);
68         socket.setNetworkInterface(networkInterface);
69         socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_SEC);
70         inetSocketAddress = new InetSocketAddress(InetAddress.getByName(AMX_MULTICAST_GROUP), AMX_MULTICAST_PORT);
71         socket.joinGroup(inetSocketAddress, null);
72         logger.debug("Multicast listener joined multicast group {}:{}", AMX_MULTICAST_GROUP, AMX_MULTICAST_PORT);
73     }
74
75     public void shutdown() {
76         logger.debug("Multicast listener closing down multicast socket");
77         try {
78             socket.leaveGroup(inetSocketAddress, null);
79             socket.close();
80         } catch (IOException e) {
81             logger.debug("Exception shutting down multicast socket: {}", e.getMessage());
82         }
83     }
84
85     /*
86      * Wait on the multicast socket for an announcement beacon. Return null on socket timeout or error.
87      * Otherwise, parse the beacon for information about the device and return the device properties.
88      */
89     public @Nullable Map<String, Object> waitForBeacon() throws IOException {
90         byte[] bytes = new byte[600];
91         boolean beaconFound;
92
93         // Wait for a device to announce itself
94         logger.trace("Multicast listener waiting for datagram on multicast port");
95         DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length);
96         try {
97             socket.receive(msgPacket);
98             beaconFound = true;
99             logger.trace("Multicast listener got datagram of length {} from multicast port: {}", msgPacket.getLength(),
100                     msgPacket.toString());
101         } catch (SocketTimeoutException e) {
102             beaconFound = false;
103         }
104
105         if (beaconFound) {
106             // Return the device properties from the announcement beacon
107             return parseAnnouncementBeacon(msgPacket);
108         }
109
110         return null;
111     }
112
113     /*
114      * Parse the announcement beacon into the elements needed to create the thing.
115      *
116      * Example beacon:
117      * AMXB<-UUID=000048746B33><-SDKClass=VideoProjector><-GUID=EPSON_EMP001><-Revision=1.0.0>
118      */
119     private @Nullable Map<String, Object> parseAnnouncementBeacon(DatagramPacket packet) {
120         String beacon = (new String(packet.getData(), StandardCharsets.UTF_8)).trim();
121         logger.trace("Multicast listener parsing announcement packet: {}", beacon);
122
123         if (beacon.toUpperCase(Locale.ENGLISH).contains("BENQ") && beacon.contains("VideoProjector")) {
124             String[] parameterList = beacon.replace(">", "").split("<-");
125
126             for (String parameter : parameterList) {
127                 String[] keyValue = parameter.split("=");
128
129                 if (keyValue.length == 2 && keyValue[0].contains("UUID") && !keyValue[1].isEmpty()) {
130                     Map<String, Object> properties = new HashMap<>();
131                     properties.put(Thing.PROPERTY_MAC_ADDRESS, keyValue[1]);
132                     properties.put(THING_PROPERTY_HOST, packet.getAddress().getHostAddress());
133                     properties.put(THING_PROPERTY_PORT, DEFAULT_PORT);
134                     return properties;
135                 }
136             }
137             logger.debug("Multicast listener doesn't know how to parse beacon: {}", beacon);
138         }
139         return null;
140     }
141
142     private NetworkInterface getMulticastInterface(String interfaceIpAddress) throws SocketException {
143         Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
144         NetworkInterface networkInterface;
145         while (networkInterfaces.hasMoreElements()) {
146             networkInterface = networkInterfaces.nextElement();
147             if (networkInterface.isLoopback()) {
148                 continue;
149             }
150             for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
151                 if (logger.isTraceEnabled()) {
152                     logger.trace("Found interface address {} -> {}", interfaceAddress.toString(),
153                             interfaceAddress.getAddress().toString());
154                 }
155                 if (interfaceAddress.getAddress().toString().endsWith("/" + interfaceIpAddress)) {
156                     return networkInterface;
157                 }
158             }
159         }
160         throw new SocketException("Unable to get network interface for " + interfaceIpAddress);
161     }
162 }