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.tacmi.internal.coe;
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.DatagramSocket;
18 import java.net.InetAddress;
19 import java.net.SocketException;
20 import java.net.SocketTimeoutException;
21 import java.util.Collection;
22 import java.util.HashSet;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.tacmi.internal.message.AnalogMessage;
29 import org.openhab.binding.tacmi.internal.message.DigitalMessage;
30 import org.openhab.binding.tacmi.internal.message.Message;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.BaseBridgeHandler;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.RefreshType;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * {@link TACmiCoEBridgeHandler} is the handler for a smarthomatic Bridge and
43 * connects it to the framework. All {@link TACmiHandler}s use the
44 * {@link TACmiCoEBridgeHandler} to execute the actual commands.
46 * @author Christian Niessner - Initial contribution
49 public class TACmiCoEBridgeHandler extends BaseBridgeHandler {
51 private final Logger logger = LoggerFactory.getLogger(TACmiCoEBridgeHandler.class);
54 * Port the C.M.I. uses for COE-Communication - this cannot be changed.
56 private static final int COE_PORT = 5441;
61 private @Nullable DatagramSocket coeSocket = null;
63 private @Nullable ReceiveThread receiveThread;
65 private @Nullable ScheduledFuture<?> timeoutCheckFuture;
67 private final Collection<TACmiHandler> registeredCMIs = new HashSet<>();
69 public TACmiCoEBridgeHandler(final Bridge br) {
74 * Thread which receives all data from the bridge.
76 private class ReceiveThread extends Thread {
77 private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
79 ReceiveThread(String threadName) {
85 final DatagramSocket coeSocket = TACmiCoEBridgeHandler.this.coeSocket;
86 if (coeSocket == null) {
87 logger.warn("coeSocket is NULL - Reader disabled!");
90 while (!isInterrupted()) {
91 final byte[] receiveData = new byte[14];
94 final DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
96 coeSocket.receive(receivePacket);
97 } catch (final SocketTimeoutException te) {
98 logger.trace("Receive timeout on CoE socket, retrying ...");
102 final byte[] data = receivePacket.getData();
104 if (data[1] > 0 && data[1] < 9) {
105 message = new AnalogMessage(data);
106 } else if (data[1] == 0 || data[1] == 9) {
107 message = new DigitalMessage(data);
109 logger.debug("Invalid message received");
112 logger.debug("{}", message.toString());
114 final InetAddress remoteAddress = receivePacket.getAddress();
115 final int node = message.canNode;
116 boolean found = false;
117 for (final TACmiHandler cmi : registeredCMIs) {
118 if (cmi.isFor(remoteAddress, node)) {
119 cmi.handleCoE(message);
124 logger.debug("Received CoE-Packet from {} Node {} and we don't have a Thing for!",
125 remoteAddress, node);
127 } catch (final IOException e) {
128 if (isInterrupted()) {
131 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
132 "Error processing data: " + e.getMessage());
134 } catch (RuntimeException e) {
135 // we catch runtime exceptions here to prevent the receiving thread to stop accidentally if
136 // something like an IllegalStateException or NumberFormatExceptions are thrown. This indicates a
137 // bug or a situation / setup I'm not thinking of ;)
138 if (isInterrupted()) {
141 logger.error("Error processing data: {}", e.getMessage(), e);
148 * Periodically check for timeouts on the registered / active CoE channels
150 private void checkForTimeouts() {
151 for (final TACmiHandler cmi : registeredCMIs) {
152 cmi.checkForTimeout();
157 public void initialize() {
159 final DatagramSocket coeSocket = new DatagramSocket(COE_PORT);
160 coeSocket.setBroadcast(true);
161 coeSocket.setSoTimeout(330000); // 300 sec is default resent-time; so we wait 330 secs
162 this.coeSocket = coeSocket;
163 } catch (final SocketException e) {
164 // logged by framework via updateStatus
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
166 "Failed to create UDP-Socket for C.M.I. CoE bridge. Reason: " + e.getMessage());
170 ReceiveThread reciveThreadNN = new ReceiveThread("OH-binding-" + getThing().getUID().getAsString());
171 reciveThreadNN.setDaemon(true);
172 reciveThreadNN.start();
173 this.receiveThread = reciveThreadNN;
175 ScheduledFuture<?> timeoutCheckFuture = this.timeoutCheckFuture;
176 if (timeoutCheckFuture == null || timeoutCheckFuture.isCancelled()) {
177 this.timeoutCheckFuture = scheduler.scheduleWithFixedDelay(this::checkForTimeouts, 1, 1, TimeUnit.SECONDS);
180 updateStatus(ThingStatus.ONLINE);
183 public void sendData(final byte[] pkt, final @Nullable InetAddress cmiAddress) throws IOException {
184 final DatagramPacket packet = new DatagramPacket(pkt, pkt.length, cmiAddress, COE_PORT);
186 DatagramSocket sock = this.coeSocket;
188 throw new IOException("Socket is closed!");
194 public void handleCommand(final ChannelUID channelUID, final Command command) {
195 if (command instanceof RefreshType) {
196 // just forward it to the registered handlers...
197 for (final TACmiHandler cmi : registeredCMIs) {
198 cmi.handleCommand(channelUID, command);
201 logger.debug("No bridge commands defined.");
205 protected void registerCMI(final TACmiHandler handler) {
206 this.registeredCMIs.add(handler);
209 protected void unregisterCMI(final TACmiHandler handler) {
210 this.registeredCMIs.remove(handler);
214 public void dispose() {
215 // clean up the timeout check
216 ScheduledFuture<?> timeoutCheckFuture = this.timeoutCheckFuture;
217 if (timeoutCheckFuture != null) {
218 timeoutCheckFuture.cancel(true);
219 this.timeoutCheckFuture = null;
222 // clean up the receive thread
223 ReceiveThread receiveThread = this.receiveThread;
224 if (receiveThread != null) {
225 receiveThread.interrupt(); // just interrupt it so when the socketException throws it's flagged as
230 DatagramSocket sock = this.coeSocket;
231 if (sock != null && !sock.isClosed()) {
233 this.coeSocket = null;
235 if (receiveThread != null) {
236 receiveThread.interrupt();
238 // it should join quite quick as we already closed the socket which should have the receiver thread
240 receiveThread.join(250);
241 } catch (final InterruptedException e) {
242 logger.debug("Unexpected interrupt in receiveThread.join(): {}", e.getMessage(), e);
244 this.receiveThread = null;