2 * Copyright (c) 2010-2021 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.mihome.internal.socket;
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.DatagramSocket;
18 import java.net.InetAddress;
19 import java.nio.charset.StandardCharsets;
21 import java.util.concurrent.CopyOnWriteArraySet;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.mihome.internal.discovery.XiaomiBridgeDiscoveryService;
26 import org.openhab.binding.mihome.internal.handler.XiaomiBridgeHandler;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
30 import com.google.gson.JsonObject;
31 import com.google.gson.JsonParser;
34 * Takes care of the communication with MiHome devices.
37 * @author Patrick Boos - Initial contribution
38 * @author Dieter Schmidt - JavaDoc, refactored, reviewed
42 public abstract class XiaomiSocket {
44 static final String MCAST_ADDR = "224.0.0.50";
46 private static final int BUFFER_LENGTH = 1024;
47 private static final JsonParser PARSER = new JsonParser();
49 private final Logger logger = LoggerFactory.getLogger(XiaomiSocket.class);
51 private final DatagramPacket datagramPacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
52 private final Set<XiaomiSocketListener> listeners = new CopyOnWriteArraySet<>();
54 private final int port;
55 private @Nullable DatagramSocket socket;
56 private final Thread socketReceiveThread = new Thread(this::receiveData);
59 * Sets up an {@link XiaomiSocket} with the MiHome multicast address and a random port
61 * @param owner identifies the socket owner
63 public XiaomiSocket(String owner) {
68 * Sets up an {@link XiaomiSocket} with the MiHome multicast address and a specific port
70 * @param port the socket will be bound to this port
71 * @param owner identifies the socket owner
73 public XiaomiSocket(int port, String owner) {
75 socketReceiveThread.setName("XiaomiSocketReceiveThread(" + port + ", " + owner + ")");
78 public void initialize() {
80 if (!socketReceiveThread.isAlive()) {
81 logger.trace("Starting receiver thread {}", socketReceiveThread);
82 socketReceiveThread.start();
86 protected abstract void setupSocket();
89 * Interrupts the {@link ReceiverThread} and closes the {@link XiaomiSocket}.
91 private void closeSocket() {
92 synchronized (XiaomiSocket.class) {
93 logger.debug("Interrupting receiver thread {}", socketReceiveThread);
94 socketReceiveThread.interrupt();
96 DatagramSocket socket = this.socket;
98 logger.debug("Closing socket {}", socket);
106 * Registers a {@link XiaomiSocketListener} to be called back, when data is received.
107 * If no {@link XiaomiSocket} exists, when the method is called, it is being set up.
109 * @param listener {@link XiaomiSocketListener} to be called back
111 public synchronized void registerListener(XiaomiSocketListener listener) {
112 if (listeners.add(listener)) {
113 logger.trace("Added socket listener {}", listener);
116 DatagramSocket socket = this.socket;
117 if (socket == null) {
123 * Unregisters a {@link XiaomiSocketListener}. If there are no listeners left,
124 * the {@link XiaomiSocket} is being closed.
126 * @param listener {@link XiaomiSocketListener} to be unregistered
128 public synchronized void unregisterListener(XiaomiSocketListener listener) {
129 if (listeners.remove(listener)) {
130 logger.trace("Removed socket listener {}", listener);
133 if (listeners.isEmpty()) {
139 * Sends a message through the {@link XiaomiSocket} to a specific address and port
141 * @param message the message to be sent
142 * @param address the message destination address
143 * @param port the message destination port
145 public void sendMessage(String message, InetAddress address, int port) {
146 DatagramSocket socket = this.socket;
147 if (socket == null) {
148 logger.error("Error while sending message (socket is null)");
153 byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
154 DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, port);
155 logger.trace("Sending message: {} to {}:{}", message, address, port);
156 socket.send(sendPacket);
157 } catch (IOException e) {
158 logger.error("Error while sending message", e);
163 * @return the port number of this {@link XiaomiSocket}
165 public int getPort() {
169 protected @Nullable DatagramSocket getSocket() {
173 protected void setSocket(DatagramSocket socket) {
174 this.socket = socket;
178 * This method is the main method of the receiver thread for the {@link XiaomiBridgeSocket}.
179 * If the socket has data, it parses the data to a json object and calls all
180 * {@link XiaomiSocketListener} and passes the data to them.
182 private void receiveData() {
183 DatagramSocket socket = this.socket;
184 if (socket == null) {
185 logger.error("Failed to receive data (socket is null)");
189 Thread currentThread = Thread.currentThread();
190 int localPort = socket.getLocalPort();
193 while (!currentThread.isInterrupted()) {
194 logger.trace("Thread {} waiting for data on port {}", currentThread.getName(), localPort);
195 socket.receive(datagramPacket);
196 InetAddress address = datagramPacket.getAddress();
197 logger.debug("Received Datagram from {}:{} on port {}", address.getHostAddress(),
198 datagramPacket.getPort(), localPort);
199 String sentence = new String(datagramPacket.getData(), 0, datagramPacket.getLength());
200 JsonObject message = PARSER.parse(sentence).getAsJsonObject();
201 notifyListeners(message, address);
202 logger.trace("Data received and notified {} listeners", listeners.size());
204 } catch (IOException e) {
205 if (!currentThread.isInterrupted()) {
206 logger.error("Error while receiving", e);
208 logger.trace("Receiver thread was interrupted");
211 logger.debug("Receiver thread ended");
215 * Notifies all {@link XiaomiSocketListener} on the parent {@link XiaomiSocket}. First checks for any matching
216 * {@link XiaomiBridgeHandler}, before passing to any {@link XiaomiBridgeDiscoveryService}.
218 * @param message the data message as {@link JsonObject}
219 * @param address the address from which the message was received
221 private void notifyListeners(JsonObject message, InetAddress address) {
222 for (XiaomiSocketListener listener : listeners) {
223 if (listener instanceof XiaomiBridgeHandler) {
224 if (((XiaomiBridgeHandler) listener).getHost().equals(address)) {
225 listener.onDataReceived(message);
227 } else if (listener instanceof XiaomiBridgeDiscoveryService) {
228 listener.onDataReceived(message);