]> git.basschouten.com Git - openhab-addons.git/blob
5b91a6896f20033e817336dcb4e1fa443a27a46e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.emotiva.internal;
14
15 import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_UDP_SENDING_TIMEOUT;
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.nio.charset.Charset;
23 import java.util.Arrays;
24 import java.util.Objects;
25 import java.util.concurrent.ExecutorService;
26 import java.util.function.Consumer;
27
28 import javax.xml.bind.JAXBException;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
33 import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionRequest;
34 import org.openhab.binding.emotiva.internal.dto.EmotivaUnsubscribeDTO;
35 import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateRequest;
36 import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
37 import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
38 import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse;
39 import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * This service handles sending UDP message to Emotiva devices.
45  *
46  * @author Patrick Koenemann - Initial contribution
47  * @author Espen Fossen - Adapted to Emotiva binding
48  */
49 @NonNullByDefault
50 public class EmotivaUdpSendingService {
51
52     private final Logger logger = LoggerFactory.getLogger(EmotivaUdpSendingService.class);
53
54     /**
55      * Buffer for incoming UDP packages.
56      */
57     private static final int MAX_PACKET_SIZE = 10240;
58
59     /**
60      * The device IP this connector is listening to / sends to.
61      */
62     private final String ipAddress;
63
64     /**
65      * The port this connector is sending to.
66      */
67     private final int sendingControlPort;
68
69     /**
70      * Service to spawn new threads for handling status updates.
71      */
72     private final ExecutorService executorService;
73
74     /**
75      * Socket for sending UDP packages.
76      */
77     private @Nullable DatagramSocket sendingSocket = null;
78
79     /**
80      * Sending response listener.
81      */
82     private @Nullable Consumer<EmotivaUdpResponse> listener;
83
84     private final EmotivaXmlUtils emotivaXmlUtils;
85
86     /**
87      * Create a socket for sending message to Emotiva device via the given configuration.
88      *
89      * @param config Emotiva configuration values
90      */
91     public EmotivaUdpSendingService(EmotivaConfiguration config, ExecutorService executorService) throws JAXBException {
92         if (config.controlPort <= 0) {
93             throw new IllegalArgumentException("Invalid udpSendingControlPort: " + config.controlPort);
94         }
95         if (config.ipAddress.trim().isEmpty()) {
96             throw new IllegalArgumentException("Missing ipAddress");
97         }
98         this.ipAddress = config.ipAddress;
99         this.sendingControlPort = config.controlPort;
100         this.executorService = executorService;
101         this.emotivaXmlUtils = new EmotivaXmlUtils();
102     }
103
104     /**
105      * Initialize socket connection to the UDP sending port
106      *
107      * @throws SocketException Is only thrown if <code>logNotThrowException = false</code>.
108      * @throws InterruptedException Typically happens during shutdown.
109      */
110     public void connect(Consumer<EmotivaUdpResponse> listener, boolean logNotThrowException)
111             throws SocketException, InterruptedException {
112         try {
113             sendingSocket = new DatagramSocket(sendingControlPort);
114
115             this.listener = listener;
116         } catch (SocketException e) {
117             disconnect();
118
119             if (!logNotThrowException) {
120                 throw e;
121             }
122         }
123     }
124
125     private void handleReceivedData(DatagramPacket answer, byte[] receivedData,
126             Consumer<EmotivaUdpResponse> localListener) {
127         // log & notify listener in new thread (so that listener loop continues immediately)
128         executorService.execute(() -> {
129             if (answer.getAddress() != null && answer.getLength() > 0) {
130                 logger.trace("Received data on port '{}': {}", answer.getPort(), receivedData);
131                 EmotivaUdpResponse emotivaUdpResponse = new EmotivaUdpResponse(
132                         new String(answer.getData(), 0, answer.getLength()), answer.getAddress().getHostAddress());
133                 localListener.accept(emotivaUdpResponse);
134             }
135         });
136     }
137
138     /**
139      * Close the socket connection.
140      */
141     public void disconnect() {
142         logger.debug("Emotiva sender stopped for '{}'", ipAddress);
143         listener = null;
144         final DatagramSocket localSendingSocket = sendingSocket;
145         if (localSendingSocket != null) {
146             synchronized (this) {
147                 if (Objects.equals(sendingSocket, localSendingSocket)) {
148                     sendingSocket = null;
149                     if (!localSendingSocket.isClosed()) {
150                         localSendingSocket.close();
151                     }
152                 }
153             }
154         }
155     }
156
157     public void send(EmotivaControlDTO dto) throws IOException {
158         send(emotivaXmlUtils.marshallJAXBElementObjects(dto));
159     }
160
161     public void sendSubscription(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException {
162         send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaSubscriptionRequest(tags, config.protocolVersion)));
163     }
164
165     public void sendUpdate(EmotivaControlCommands defaultCommand, EmotivaConfiguration config) throws IOException {
166         send(emotivaXmlUtils
167                 .marshallJAXBElementObjects(new EmotivaUpdateRequest(defaultCommand, config.protocolVersion)));
168     }
169
170     public void sendUpdate(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException {
171         send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUpdateRequest(tags, config.protocolVersion)));
172     }
173
174     public void sendUnsubscribe(EmotivaSubscriptionTags[] defaultCommand) throws IOException {
175         send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUnsubscribeDTO(defaultCommand)));
176     }
177
178     public void send(String msg) throws IOException {
179         logger.trace("Sending message '{}' to {}:{}", msg, ipAddress, sendingControlPort);
180         if (msg.isEmpty()) {
181             throw new IllegalArgumentException("Message must not be empty");
182         }
183
184         final InetAddress ipAddress = InetAddress.getByName(this.ipAddress);
185         byte[] buf = msg.getBytes(Charset.defaultCharset());
186         DatagramPacket packet = new DatagramPacket(buf, buf.length, ipAddress, sendingControlPort);
187
188         // make sure we are not interrupted by a disconnect while sending this message
189         synchronized (this) {
190             DatagramSocket localDatagramSocket = this.sendingSocket;
191             final DatagramPacket answer = new DatagramPacket(new byte[MAX_PACKET_SIZE], MAX_PACKET_SIZE);
192             final Consumer<EmotivaUdpResponse> localListener = listener;
193             if (localDatagramSocket != null && !localDatagramSocket.isClosed()) {
194                 localDatagramSocket.setSoTimeout(DEFAULT_UDP_SENDING_TIMEOUT);
195                 localDatagramSocket.send(packet);
196                 logger.debug("Sending successful");
197
198                 localDatagramSocket.receive(answer);
199                 final byte[] receivedData = Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1);
200
201                 if (receivedData.length == 0) {
202                     logger.debug("Nothing received, this may happen during shutdown or some unknown error");
203                 }
204
205                 if (localListener != null) {
206                     handleReceivedData(answer, receivedData, localListener);
207                 }
208             } else {
209                 throw new SocketException("Datagram Socket closed or not initialized");
210             }
211         }
212     }
213 }