]> git.basschouten.com Git - openhab-addons.git/blob
6e92226c72504d66fd7fd1ec88557e1a11ec5d62
[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.luxom.internal.handler;
14
15 import java.io.IOException;
16 import java.util.List;
17 import java.util.concurrent.BlockingQueue;
18 import java.util.concurrent.LinkedBlockingQueue;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.atomic.AtomicInteger;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.luxom.internal.handler.config.LuxomBridgeConfig;
26 import org.openhab.binding.luxom.internal.protocol.LuxomAction;
27 import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
28 import org.openhab.binding.luxom.internal.protocol.LuxomCommunication;
29 import org.openhab.binding.luxom.internal.protocol.LuxomSystemInfo;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
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.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * Handler responsible for communicating with the main Luxom IP access module.
42  *
43  * @author Kris Jespers - Initial contribution
44  */
45 @NonNullByDefault
46 public class LuxomBridgeHandler extends BaseBridgeHandler {
47     public static final int HEARTBEAT_INTERVAL_SECONDS = 50;
48     private final LuxomSystemInfo systemInfo;
49
50     private static final int DEFAULT_RECONNECT_INTERVAL_IN_MINUTES = 1;
51     private static final long HEARTBEAT_ACK_TIMEOUT_SECONDS = 20;
52
53     private final Logger logger = LoggerFactory.getLogger(LuxomBridgeHandler.class);
54
55     private @Nullable LuxomBridgeConfig config;
56     private final AtomicInteger nrOfSendPermits = new AtomicInteger(0);
57     private int reconnectInterval;
58
59     private @Nullable LuxomCommand previousCommand;
60     private final LuxomCommunication communication;
61     private final BlockingQueue<List<CommandExecutionSpecification>> sendQueue = new LinkedBlockingQueue<>();
62
63     private @Nullable Thread messageSender;
64     private @Nullable ScheduledFuture<?> heartBeat;
65     private @Nullable ScheduledFuture<?> heartBeatTimeoutTask;
66     private @Nullable ScheduledFuture<?> connectRetryJob;
67
68     public @Nullable LuxomBridgeConfig getIPBridgeConfig() {
69         return config;
70     }
71
72     public LuxomBridgeHandler(Bridge bridge) {
73         super(bridge);
74         logger.debug("Luxom bridge init");
75         systemInfo = new LuxomSystemInfo();
76         communication = new LuxomCommunication(this);
77     }
78
79     @Override
80     public void handleCommand(ChannelUID channelUID, Command command) {
81         logger.debug("Bridge received command {} for {}", command.toFullString(), channelUID);
82     }
83
84     @Override
85     public void initialize() {
86         config = getConfig().as(LuxomBridgeConfig.class);
87
88         if (validConfiguration(config)) {
89             reconnectInterval = (config.reconnectInterval > 0) ? config.reconnectInterval
90                     : DEFAULT_RECONNECT_INTERVAL_IN_MINUTES;
91
92             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.connecting");
93             scheduler.submit(this::connect); // start the async connect task
94         }
95     }
96
97     private boolean validConfiguration(@Nullable LuxomBridgeConfig config) {
98         if (config == null) {
99             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
100                     "@text/bridge-configuration-missing");
101
102             return false;
103         }
104
105         if (config.ipAddress == null || config.ipAddress.trim().isEmpty()) {
106             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/bridge-address-missing");
107
108             return false;
109         }
110
111         return true;
112     }
113
114     private void scheduleConnectRetry(long waitMinutes) {
115         logger.debug("Scheduling connection retry in {} (minutes)", waitMinutes);
116         connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
117     }
118
119     private synchronized void connect() {
120         if (communication.isConnected()) {
121             return;
122         }
123
124         if (config != null) {
125             logger.debug("Connecting to bridge at {}", config.ipAddress);
126         }
127
128         try {
129             communication.startCommunication();
130         } catch (Exception e) {
131             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
132             disconnect();
133             scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
134         }
135     }
136
137     public void startProcessing() {
138         nrOfSendPermits.set(1);
139
140         updateStatus(ThingStatus.ONLINE);
141
142         messageSender = new Thread(this::sendCommandsThread, "Luxom sender");
143         messageSender.start();
144
145         logger.debug("Starting heartbeat job with interval {} (seconds)", HEARTBEAT_INTERVAL_SECONDS);
146         heartBeat = scheduler.scheduleWithFixedDelay(this::sendHeartBeat, 10, HEARTBEAT_INTERVAL_SECONDS,
147                 TimeUnit.SECONDS);
148     }
149
150     private void sendCommandsThread() {
151         logger.debug("Starting send commands thread...");
152         try {
153             while (!Thread.currentThread().isInterrupted()) {
154                 logger.debug("waiting for command to send...");
155                 List<CommandExecutionSpecification> commands = sendQueue.take();
156
157                 try {
158                     for (CommandExecutionSpecification commandExecutionSpecification : commands) {
159                         communication.sendMessage(commandExecutionSpecification.getCommand());
160                     }
161                 } catch (IOException e) {
162                     logger.warn("Communication error while sending, will try to reconnect. Error: {}", e.getMessage());
163                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
164
165                     reconnect();
166
167                     // reconnect() will start a new thread; terminate this one
168                     break;
169                 }
170             }
171         } catch (InterruptedException e) {
172             Thread.currentThread().interrupt();
173         }
174     }
175
176     private synchronized void disconnect() {
177         logger.debug("Disconnecting from bridge");
178
179         if (connectRetryJob != null) {
180             connectRetryJob.cancel(true);
181         }
182
183         if (this.heartBeat != null) {
184             this.heartBeat.cancel(true);
185         }
186
187         cancelCheckAliveTimeoutTask();
188
189         if (messageSender != null && messageSender.isAlive()) {
190             messageSender.interrupt();
191         }
192
193         this.communication.stopCommunication();
194     }
195
196     public void reconnect() {
197         reconnect(false);
198     }
199
200     private synchronized void reconnect(boolean timeout) {
201         if (timeout) {
202             logger.debug("Keepalive timeout, attempting to reconnect to the bridge");
203         } else {
204             logger.debug("Connection problem, attempting to reconnect to the bridge");
205         }
206
207         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
208         disconnect();
209         connect();
210     }
211
212     public void sendCommands(List<CommandExecutionSpecification> commands) {
213         this.sendQueue.add(commands);
214     }
215
216     private @Nullable LuxomThingHandler findThingHandler(@Nullable String address) {
217         for (Thing thing : getThing().getThings()) {
218             if (thing.getHandler() instanceof LuxomThingHandler) {
219                 LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
220
221                 try {
222                     if (handler != null && handler.getAddress().equals(address)) {
223                         return handler;
224                     }
225                 } catch (IllegalStateException e) {
226                     logger.trace("Handler for id {} not initialized", address);
227                 }
228             }
229         }
230
231         return null;
232     }
233
234     /**
235      * needed with fast reconnect to update status of things
236      */
237     public void forceRefreshThings() {
238         for (Thing thing : getThing().getThings()) {
239             if (thing.getHandler() instanceof LuxomThingHandler) {
240                 LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
241                 handler.ping();
242             }
243         }
244     }
245
246     private void sendHeartBeat() {
247         logger.trace("Sending heartbeat");
248         // Reconnect if no response is received within KEEPALIVE_TIMEOUT_SECONDS.
249         heartBeatTimeoutTask = scheduler.schedule(() -> reconnect(true), HEARTBEAT_ACK_TIMEOUT_SECONDS,
250                 TimeUnit.SECONDS);
251         sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.HEARTBEAT.getCommand())));
252     }
253
254     @Override
255     public void thingUpdated(Thing thing) {
256         LuxomBridgeConfig newConfig = thing.getConfiguration().as(LuxomBridgeConfig.class);
257         boolean validConfig = validConfiguration(newConfig);
258         boolean needsReconnect = validConfig && config != null && !config.sameConnectionParameters(newConfig);
259
260         if (!validConfig || needsReconnect) {
261             dispose();
262         }
263
264         this.thing = thing;
265         this.config = newConfig;
266
267         if (needsReconnect) {
268             initialize();
269         }
270     }
271
272     public void handleCommunicationError(IOException e) {
273         logger.debug("Communication error while reading, will try to reconnect. Error: {}", e.getMessage());
274         reconnect();
275     }
276
277     @Override
278     public void dispose() {
279         disconnect();
280     }
281
282     public void handleIncomingLuxomMessage(String luxomMessage) throws IOException {
283         cancelCheckAliveTimeoutTask(); // we got a message
284
285         logger.trace("Luxom: received {}", luxomMessage);
286         LuxomCommand luxomCommand = new LuxomCommand(luxomMessage);
287
288         // Now dispatch update to the proper thing handler
289
290         if (LuxomAction.PASSWORD_REQUEST == luxomCommand.getAction()) {
291             communication.sendMessage(LuxomAction.REQUEST_FOR_INFORMATION.getCommand()); // direct send, no queue, so
292             // no tcp flow constraint
293         } else if (LuxomAction.MODULE_INFORMATION == luxomCommand.getAction()) {
294             cmdSystemInfo(luxomCommand.getData());
295             if (ThingStatus.ONLINE != getThing().getStatus()) {
296                 // this all happens before TCP flow controle, when startProcessing is called, TCP flow is activated...
297                 startProcessing();
298             }
299         } else if (LuxomAction.ACKNOWLEDGE == luxomCommand.getAction()) {
300             logger.trace("received acknowledgement");
301         } else if (LuxomAction.DATA == luxomCommand.getAction()
302                 || LuxomAction.DATA_RESPONSE == luxomCommand.getAction()) {
303             previousCommand = luxomCommand;
304         } else if (LuxomAction.INVALID_ACTION != luxomCommand.getAction()) {
305             if (LuxomAction.DATA_BYTE == luxomCommand.getAction()
306                     || LuxomAction.DATA_BYTE_RESPONSE == luxomCommand.getAction()) {
307                 // data for previous command if it needs it
308                 if (previousCommand != null && previousCommand.getAction().isNeedsData()) {
309                     previousCommand.setData(luxomCommand.getData());
310                     luxomCommand = previousCommand;
311                     previousCommand = null;
312                 }
313             }
314
315             if (luxomCommand != null) {
316                 LuxomThingHandler handler = findThingHandler(luxomCommand.getAddress());
317
318                 if (handler != null) {
319                     handler.handleCommandComingFromBridge(luxomCommand);
320                 } else {
321                     logger.warn("No handler found command {} for address : {}", luxomMessage,
322                             luxomCommand.getAddress());
323                 }
324             } else {
325                 logger.warn("Something was wrong with the order of incoming commands, resulting command is null");
326             }
327         } else {
328             logger.trace("Luxom: not handled {}", luxomMessage);
329         }
330         logger.trace("nrOfPermits after receive: {}", nrOfSendPermits.get());
331     }
332
333     private void cancelCheckAliveTimeoutTask() {
334         var task = heartBeatTimeoutTask;
335         if (task != null) {
336             // This method can be called from the keepAliveReconnect thread. Make sure
337             // we don't interrupt ourselves, as that may prevent the reconnection attempt.
338             task.cancel(false);
339         }
340     }
341
342     private synchronized void cmdSystemInfo(@Nullable String info) {
343         systemInfo.setSwVersion(info);
344     }
345 }