]> git.basschouten.com Git - openhab-addons.git/blob
c0c9483bf7cf9984b2756e720e44d7f06e04aa50
[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.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
48     private final Logger logger = LoggerFactory.getLogger(XiaomiSocket.class);
49
50     private final DatagramPacket datagramPacket = new DatagramPacket(new byte[BUFFER_LENGTH], BUFFER_LENGTH);
51     private final Set<XiaomiSocketListener> listeners = new CopyOnWriteArraySet<>();
52
53     private final int port;
54     private @Nullable DatagramSocket socket;
55     private final Thread socketReceiveThread = new Thread(this::receiveData);
56
57     /**
58      * Sets up a {@link XiaomiSocket} with the MiHome multicast address and a random port
59      *
60      * @param owner identifies the socket owner
61      */
62     public XiaomiSocket(String owner) {
63         this(0, owner);
64     }
65
66     /**
67      * Sets up a {@link XiaomiSocket} with the MiHome multicast address and a specific port
68      *
69      * @param port the socket will be bound to this port
70      * @param owner identifies the socket owner
71      */
72     public XiaomiSocket(int port, String owner) {
73         this.port = port;
74         socketReceiveThread.setName("XiaomiSocketReceiveThread(" + port + ", " + owner + ")");
75     }
76
77     public void initialize() {
78         setupSocket();
79         if (!socketReceiveThread.isAlive()) {
80             logger.trace("Starting receiver thread {}", socketReceiveThread);
81             socketReceiveThread.start();
82         }
83     }
84
85     protected abstract void setupSocket();
86
87     /**
88      * Interrupts the {@link ReceiverThread} and closes the {@link XiaomiSocket}.
89      */
90     private void closeSocket() {
91         synchronized (XiaomiSocket.class) {
92             logger.debug("Interrupting receiver thread {}", socketReceiveThread);
93             socketReceiveThread.interrupt();
94
95             DatagramSocket socket = this.socket;
96             if (socket != null) {
97                 logger.debug("Closing socket {}", socket);
98                 socket.close();
99                 this.socket = null;
100             }
101         }
102     }
103
104     /**
105      * Registers a {@link XiaomiSocketListener} to be called back, when data is received.
106      * If no {@link XiaomiSocket} exists, when the method is called, it is being set up.
107      *
108      * @param listener {@link XiaomiSocketListener} to be called back
109      */
110     public synchronized void registerListener(XiaomiSocketListener listener) {
111         if (listeners.add(listener)) {
112             logger.trace("Added socket listener {}", listener);
113         }
114
115         DatagramSocket socket = this.socket;
116         if (socket == null) {
117             initialize();
118         }
119     }
120
121     /**
122      * Unregisters a {@link XiaomiSocketListener}. If there are no listeners left,
123      * the {@link XiaomiSocket} is being closed.
124      *
125      * @param listener {@link XiaomiSocketListener} to be unregistered
126      */
127     public synchronized void unregisterListener(XiaomiSocketListener listener) {
128         if (listeners.remove(listener)) {
129             logger.trace("Removed socket listener {}", listener);
130         }
131
132         if (listeners.isEmpty()) {
133             closeSocket();
134         }
135     }
136
137     /**
138      * Sends a message through the {@link XiaomiSocket} to a specific address and port
139      *
140      * @param message the message to be sent
141      * @param address the message destination address
142      * @param port the message destination port
143      */
144     public void sendMessage(String message, InetAddress address, int port) {
145         DatagramSocket socket = this.socket;
146         if (socket == null) {
147             logger.error("Error while sending message (socket is null)");
148             return;
149         }
150
151         try {
152             byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
153             DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, port);
154             logger.trace("Sending message: {} to {}:{}", message, address, port);
155             socket.send(sendPacket);
156         } catch (IOException e) {
157             logger.error("Error while sending message", e);
158         }
159     }
160
161     /**
162      * @return the port number of this {@link XiaomiSocket}
163      */
164     public int getPort() {
165         return port;
166     }
167
168     protected @Nullable DatagramSocket getSocket() {
169         return socket;
170     }
171
172     protected void setSocket(DatagramSocket socket) {
173         this.socket = socket;
174     }
175
176     /**
177      * This method is the main method of the receiver thread for the {@link XiaomiBridgeSocket}.
178      * If the socket has data, it parses the data to a json object and calls all
179      * {@link XiaomiSocketListener} and passes the data to them.
180      */
181     private void receiveData() {
182         DatagramSocket socket = this.socket;
183         if (socket == null) {
184             logger.error("Failed to receive data (socket is null)");
185             return;
186         }
187
188         Thread currentThread = Thread.currentThread();
189         int localPort = socket.getLocalPort();
190
191         try {
192             while (!currentThread.isInterrupted()) {
193                 logger.trace("Thread {} waiting for data on port {}", currentThread.getName(), localPort);
194                 socket.receive(datagramPacket);
195                 InetAddress address = datagramPacket.getAddress();
196                 logger.debug("Received Datagram from {}:{} on port {}", address.getHostAddress(),
197                         datagramPacket.getPort(), localPort);
198                 String sentence = new String(datagramPacket.getData(), 0, datagramPacket.getLength());
199                 JsonObject message = JsonParser.parseString(sentence).getAsJsonObject();
200                 notifyListeners(message, address);
201                 logger.trace("Data received and notified {} listeners", listeners.size());
202             }
203         } catch (IOException e) {
204             if (!currentThread.isInterrupted()) {
205                 logger.error("Error while receiving", e);
206             } else {
207                 logger.trace("Receiver thread was interrupted");
208             }
209         }
210         logger.debug("Receiver thread ended");
211     }
212
213     /**
214      * Notifies all {@link XiaomiSocketListener} on the parent {@link XiaomiSocket}. First checks for any matching
215      * {@link XiaomiBridgeHandler}, before passing to any {@link XiaomiBridgeDiscoveryService}.
216      *
217      * @param message the data message as {@link JsonObject}
218      * @param address the address from which the message was received
219      */
220     private void notifyListeners(JsonObject message, InetAddress address) {
221         for (XiaomiSocketListener listener : listeners) {
222             if (listener instanceof XiaomiBridgeHandler handler) {
223                 if (handler.getHost().equals(address)) {
224                     listener.onDataReceived(message);
225                 }
226             } else if (listener instanceof XiaomiBridgeDiscoveryService) {
227                 listener.onDataReceived(message);
228             }
229         }
230     }
231 }