]> git.basschouten.com Git - openhab-addons.git/blob
abd3cb718bdc7b39e8a2ed6690e329e3add28180
[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.tacmi.internal.coe;
14
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;
25
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;
40
41 /**
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.
45  *
46  * @author Christian Niessner - Initial contribution
47  */
48 @NonNullByDefault
49 public class TACmiCoEBridgeHandler extends BaseBridgeHandler {
50
51     private final Logger logger = LoggerFactory.getLogger(TACmiCoEBridgeHandler.class);
52
53     /**
54      * Port the C.M.I. uses for COE-Communication - this cannot be changed.
55      */
56     private static final int COE_PORT = 5441;
57
58     /**
59      * Connection socket
60      */
61     private @Nullable DatagramSocket coeSocket = null;
62
63     private @Nullable ReceiveThread receiveThread;
64
65     private @Nullable ScheduledFuture<?> timeoutCheckFuture;
66
67     private final Collection<TACmiHandler> registeredCMIs = new HashSet<>();
68
69     public TACmiCoEBridgeHandler(final Bridge br) {
70         super(br);
71     }
72
73     /**
74      * Thread which receives all data from the bridge.
75      */
76     private class ReceiveThread extends Thread {
77         private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
78
79         ReceiveThread(String threadName) {
80             super(threadName);
81         }
82
83         @Override
84         public void run() {
85             final DatagramSocket coeSocket = TACmiCoEBridgeHandler.this.coeSocket;
86             if (coeSocket == null) {
87                 logger.warn("coeSocket is NULL - Reader disabled!");
88                 return;
89             }
90             while (!isInterrupted()) {
91                 final byte[] receiveData = new byte[14];
92
93                 try {
94                     final DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
95                     try {
96                         coeSocket.receive(receivePacket);
97                     } catch (final SocketTimeoutException te) {
98                         logger.trace("Receive timeout on CoE socket, retrying ...");
99                         continue;
100                     }
101
102                     final byte[] data = receivePacket.getData();
103                     Message message;
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);
108                     } else {
109                         logger.debug("Invalid message received");
110                         continue;
111                     }
112                     logger.debug("{}", message.toString());
113
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);
120                             found = true;
121                         }
122                     }
123                     if (!found) {
124                         logger.debug("Received CoE-Packet from {} Node {} and we don't have a Thing for!",
125                                 remoteAddress, node);
126                     }
127                 } catch (final IOException e) {
128                     if (isInterrupted()) {
129                         return;
130                     }
131                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
132                             "Error processing data: " + e.getMessage());
133
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()) {
139                         return;
140                     }
141                     logger.error("Error processing data: {}", e.getMessage(), e);
142                 }
143             }
144         }
145     }
146
147     /**
148      * Periodically check for timeouts on the registered / active CoE channels
149      */
150     private void checkForTimeouts() {
151         for (final TACmiHandler cmi : registeredCMIs) {
152             cmi.checkForTimeout();
153         }
154     }
155
156     @Override
157     public void initialize() {
158         try {
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());
167             return;
168         }
169
170         ReceiveThread reciveThreadNN = new ReceiveThread("OH-binding-" + getThing().getUID().getAsString());
171         reciveThreadNN.setDaemon(true);
172         reciveThreadNN.start();
173         this.receiveThread = reciveThreadNN;
174
175         ScheduledFuture<?> timeoutCheckFuture = this.timeoutCheckFuture;
176         if (timeoutCheckFuture == null || timeoutCheckFuture.isCancelled()) {
177             this.timeoutCheckFuture = scheduler.scheduleWithFixedDelay(this::checkForTimeouts, 1, 1, TimeUnit.SECONDS);
178         }
179
180         updateStatus(ThingStatus.ONLINE);
181     }
182
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);
185         @Nullable
186         DatagramSocket sock = this.coeSocket;
187         if (sock == null) {
188             throw new IOException("Socket is closed!");
189         }
190         sock.send(packet);
191     }
192
193     @Override
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);
199             }
200         } else {
201             logger.debug("No bridge commands defined.");
202         }
203     }
204
205     protected void registerCMI(final TACmiHandler handler) {
206         this.registeredCMIs.add(handler);
207     }
208
209     protected void unregisterCMI(final TACmiHandler handler) {
210         this.registeredCMIs.remove(handler);
211     }
212
213     @Override
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;
220         }
221
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
226                                        // interrupted.
227         }
228
229         @Nullable
230         DatagramSocket sock = this.coeSocket;
231         if (sock != null && !sock.isClosed()) {
232             sock.close();
233             this.coeSocket = null;
234         }
235         if (receiveThread != null) {
236             receiveThread.interrupt();
237             try {
238                 // it should join quite quick as we already closed the socket which should have the receiver thread
239                 // caused to stop.
240                 receiveThread.join(250);
241             } catch (final InterruptedException e) {
242                 logger.debug("Unexpected interrupt in receiveThread.join(): {}", e.getMessage(), e);
243             }
244             this.receiveThread = null;
245         }
246         super.dispose();
247     }
248 }