2 * Copyright (c) 2010-2024 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.emotiva.internal;
15 import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_UDP_SENDING_TIMEOUT;
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;
28 import javax.xml.bind.JAXBException;
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;
44 * This service handles sending UDP message to Emotiva devices.
46 * @author Patrick Koenemann - Initial contribution
47 * @author Espen Fossen - Adapted to Emotiva binding
50 public class EmotivaUdpSendingService {
52 private final Logger logger = LoggerFactory.getLogger(EmotivaUdpSendingService.class);
55 * Buffer for incoming UDP packages.
57 private static final int MAX_PACKET_SIZE = 10240;
60 * The device IP this connector is listening to / sends to.
62 private final String ipAddress;
65 * The port this connector is sending to.
67 private final int sendingControlPort;
70 * Service to spawn new threads for handling status updates.
72 private final ExecutorService executorService;
75 * Socket for sending UDP packages.
77 private @Nullable DatagramSocket sendingSocket = null;
80 * Sending response listener.
82 private @Nullable Consumer<EmotivaUdpResponse> listener;
84 private final EmotivaXmlUtils emotivaXmlUtils;
87 * Create a socket for sending message to Emotiva device via the given configuration.
89 * @param config Emotiva configuration values
91 public EmotivaUdpSendingService(EmotivaConfiguration config, ExecutorService executorService) throws JAXBException {
92 if (config.controlPort <= 0) {
93 throw new IllegalArgumentException("Invalid udpSendingControlPort: " + config.controlPort);
95 if (config.ipAddress.trim().isEmpty()) {
96 throw new IllegalArgumentException("Missing ipAddress");
98 this.ipAddress = config.ipAddress;
99 this.sendingControlPort = config.controlPort;
100 this.executorService = executorService;
101 this.emotivaXmlUtils = new EmotivaXmlUtils();
105 * Initialize socket connection to the UDP sending port
107 * @throws SocketException Is only thrown if <code>logNotThrowException = false</code>.
108 * @throws InterruptedException Typically happens during shutdown.
110 public void connect(Consumer<EmotivaUdpResponse> listener, boolean logNotThrowException)
111 throws SocketException, InterruptedException {
113 sendingSocket = new DatagramSocket(sendingControlPort);
115 this.listener = listener;
116 } catch (SocketException e) {
119 if (!logNotThrowException) {
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);
139 * Close the socket connection.
141 public void disconnect() {
142 logger.debug("Emotiva sender stopped for '{}'", ipAddress);
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();
157 public void send(EmotivaControlDTO dto) throws IOException {
158 send(emotivaXmlUtils.marshallJAXBElementObjects(dto));
161 public void sendSubscription(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException {
162 send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaSubscriptionRequest(tags, config.protocolVersion)));
165 public void sendUpdate(EmotivaControlCommands defaultCommand, EmotivaConfiguration config) throws IOException {
167 .marshallJAXBElementObjects(new EmotivaUpdateRequest(defaultCommand, config.protocolVersion)));
170 public void sendUpdate(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException {
171 send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUpdateRequest(tags, config.protocolVersion)));
174 public void sendUnsubscribe(EmotivaSubscriptionTags[] defaultCommand) throws IOException {
175 send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUnsubscribeDTO(defaultCommand)));
178 public void send(String msg) throws IOException {
179 logger.trace("Sending message '{}' to {}:{}", msg, ipAddress, sendingControlPort);
181 throw new IllegalArgumentException("Message must not be empty");
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);
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");
198 localDatagramSocket.receive(answer);
199 final byte[] receivedData = Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1);
201 if (receivedData.length == 0) {
202 logger.debug("Nothing received, this may happen during shutdown or some unknown error");
205 if (localListener != null) {
206 handleReceivedData(answer, receivedData, localListener);
209 throw new SocketException("Datagram Socket closed or not initialized");