]> git.basschouten.com Git - openhab-addons.git/blob
b6f50e3ed002165706fe6263d2cc0325c20a9569
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.mihome.internal.socket;
14
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;
20 import java.util.Set;
21 import java.util.concurrent.CopyOnWriteArraySet;
22
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;
29
30 import com.google.gson.JsonObject;
31 import com.google.gson.JsonParser;
32
33 /**
34  * Takes care of the communication with MiHome devices.
35  *
36  *
37  * @author Patrick Boos - Initial contribution
38  * @author Dieter Schmidt - JavaDoc, refactored, reviewed
39  *
40  */
41 @NonNullByDefault
42 public abstract class XiaomiSocket {
43
44     static final String MCAST_ADDR = "224.0.0.50";
45
46     private static final int BUFFER_LENGTH = 1024;
47     private static final JsonParser PARSER = new JsonParser();
48
49     private final Logger logger = LoggerFactory.getLogger(XiaomiSocket.class);
50
51     private final DatagramPacket datagramPacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
52     private final Set<XiaomiSocketListener> listeners = new CopyOnWriteArraySet<>();
53
54     private final int port;
55     private @Nullable DatagramSocket socket;
56     private final Thread socketReceiveThread = new Thread(this::receiveData);
57
58     /**
59      * Sets up an {@link XiaomiSocket} with the MiHome multicast address and a random port
60      *
61      * @param owner identifies the socket owner
62      */
63     public XiaomiSocket(String owner) {
64         this(0, owner);
65     }
66
67     /**
68      * Sets up an {@link XiaomiSocket} with the MiHome multicast address and a specific port
69      *
70      * @param port the socket will be bound to this port
71      * @param owner identifies the socket owner
72      */
73     public XiaomiSocket(int port, String owner) {
74         this.port = port;
75         socketReceiveThread.setName("XiaomiSocketReceiveThread(" + port + ", " + owner + ")");
76     }
77
78     public void initialize() {
79         setupSocket();
80         if (!socketReceiveThread.isAlive()) {
81             logger.trace("Starting receiver thread {}", socketReceiveThread);
82             socketReceiveThread.start();
83         }
84     }
85
86     protected abstract void setupSocket();
87
88     /**
89      * Interrupts the {@link ReceiverThread} and closes the {@link XiaomiSocket}.
90      */
91     private void closeSocket() {
92         synchronized (XiaomiSocket.class) {
93             logger.debug("Interrupting receiver thread {}", socketReceiveThread);
94             socketReceiveThread.interrupt();
95
96             DatagramSocket socket = this.socket;
97             if (socket != null) {
98                 logger.debug("Closing socket {}", socket);
99                 socket.close();
100                 this.socket = null;
101             }
102         }
103     }
104
105     /**
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.
108      *
109      * @param listener {@link XiaomiSocketListener} to be called back
110      */
111     public synchronized void registerListener(XiaomiSocketListener listener) {
112         if (listeners.add(listener)) {
113             logger.trace("Added socket listener {}", listener);
114         }
115
116         DatagramSocket socket = this.socket;
117         if (socket == null) {
118             initialize();
119         }
120     }
121
122     /**
123      * Unregisters a {@link XiaomiSocketListener}. If there are no listeners left,
124      * the {@link XiaomiSocket} is being closed.
125      *
126      * @param listener {@link XiaomiSocketListener} to be unregistered
127      */
128     public synchronized void unregisterListener(XiaomiSocketListener listener) {
129         if (listeners.remove(listener)) {
130             logger.trace("Removed socket listener {}", listener);
131         }
132
133         if (listeners.isEmpty()) {
134             closeSocket();
135         }
136     }
137
138     /**
139      * Sends a message through the {@link XiaomiSocket} to a specific address and port
140      *
141      * @param message the message to be sent
142      * @param address the message destination address
143      * @param port the message destination port
144      */
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)");
149             return;
150         }
151
152         try {
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);
159         }
160     }
161
162     /**
163      * @return the port number of this {@link XiaomiSocket}
164      */
165     public int getPort() {
166         return port;
167     }
168
169     protected @Nullable DatagramSocket getSocket() {
170         return socket;
171     }
172
173     protected void setSocket(DatagramSocket socket) {
174         this.socket = socket;
175     }
176
177     /**
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.
181      */
182     private void receiveData() {
183         DatagramSocket socket = this.socket;
184         if (socket == null) {
185             logger.error("Failed to receive data (socket is null)");
186             return;
187         }
188
189         Thread currentThread = Thread.currentThread();
190         int localPort = socket.getLocalPort();
191
192         try {
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());
203             }
204         } catch (IOException e) {
205             if (!currentThread.isInterrupted()) {
206                 logger.error("Error while receiving", e);
207             } else {
208                 logger.trace("Receiver thread was interrupted");
209             }
210         }
211         logger.debug("Receiver thread ended");
212     }
213
214     /**
215      * Notifies all {@link XiaomiSocketListener} on the parent {@link XiaomiSocket}. First checks for any matching
216      * {@link XiaomiBridgeHandler}, before passing to any {@link XiaomiBridgeDiscoveryService}.
217      *
218      * @param message the data message as {@link JsonObject}
219      * @param address the address from which the message was received
220      */
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);
226                 }
227             } else if (listener instanceof XiaomiBridgeDiscoveryService) {
228                 listener.onDataReceived(message);
229             }
230         }
231     }
232 }