2 * Copyright (c) 2010-2023 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.luxom.internal.handler;
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;
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;
41 * Handler responsible for communicating with the main Luxom IP access module.
43 * @author Kris Jespers - Initial contribution
46 public class LuxomBridgeHandler extends BaseBridgeHandler {
47 public static final int HEARTBEAT_INTERVAL_SECONDS = 50;
48 private final LuxomSystemInfo systemInfo;
50 private static final int DEFAULT_RECONNECT_INTERVAL_IN_MINUTES = 1;
51 private static final long HEARTBEAT_ACK_TIMEOUT_SECONDS = 20;
53 private final Logger logger = LoggerFactory.getLogger(LuxomBridgeHandler.class);
55 private @Nullable LuxomBridgeConfig config;
56 private final AtomicInteger nrOfSendPermits = new AtomicInteger(0);
57 private int reconnectInterval;
59 private @Nullable LuxomCommand previousCommand;
60 private final LuxomCommunication communication;
61 private final BlockingQueue<List<CommandExecutionSpecification>> sendQueue = new LinkedBlockingQueue<>();
63 private @Nullable Thread messageSender;
64 private @Nullable ScheduledFuture<?> heartBeat;
65 private @Nullable ScheduledFuture<?> heartBeatTimeoutTask;
66 private @Nullable ScheduledFuture<?> connectRetryJob;
68 public @Nullable LuxomBridgeConfig getIPBridgeConfig() {
72 public LuxomBridgeHandler(Bridge bridge) {
74 logger.debug("Luxom bridge init");
75 systemInfo = new LuxomSystemInfo();
76 communication = new LuxomCommunication(this);
80 public void handleCommand(ChannelUID channelUID, Command command) {
81 logger.debug("Bridge received command {} for {}", command.toFullString(), channelUID);
85 public void initialize() {
86 config = getConfig().as(LuxomBridgeConfig.class);
88 if (validConfiguration(config)) {
89 reconnectInterval = (config.reconnectInterval > 0) ? config.reconnectInterval
90 : DEFAULT_RECONNECT_INTERVAL_IN_MINUTES;
92 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.connecting");
93 scheduler.submit(this::connect); // start the async connect task
97 private boolean validConfiguration(@Nullable LuxomBridgeConfig config) {
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
100 "@text/bridge-configuration-missing");
105 if (config.ipAddress == null || config.ipAddress.trim().isEmpty()) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/bridge-address-missing");
114 private void scheduleConnectRetry(long waitMinutes) {
115 logger.debug("Scheduling connection retry in {} (minutes)", waitMinutes);
116 connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
119 private synchronized void connect() {
120 if (communication.isConnected()) {
124 if (config != null) {
125 logger.debug("Connecting to bridge at {}", config.ipAddress);
129 communication.startCommunication();
130 } catch (Exception e) {
131 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
133 scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
137 public void startProcessing() {
138 nrOfSendPermits.set(1);
140 updateStatus(ThingStatus.ONLINE);
142 messageSender = new Thread(this::sendCommandsThread, "Luxom sender");
143 messageSender.start();
145 logger.debug("Starting heartbeat job with interval {} (seconds)", HEARTBEAT_INTERVAL_SECONDS);
146 heartBeat = scheduler.scheduleWithFixedDelay(this::sendHeartBeat, 10, HEARTBEAT_INTERVAL_SECONDS,
150 private void sendCommandsThread() {
151 logger.debug("Starting send commands thread...");
153 while (!Thread.currentThread().isInterrupted()) {
154 logger.debug("waiting for command to send...");
155 List<CommandExecutionSpecification> commands = sendQueue.take();
158 for (CommandExecutionSpecification commandExecutionSpecification : commands) {
159 communication.sendMessage(commandExecutionSpecification.getCommand());
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);
167 // reconnect() will start a new thread; terminate this one
171 } catch (InterruptedException e) {
172 Thread.currentThread().interrupt();
176 private synchronized void disconnect() {
177 logger.debug("Disconnecting from bridge");
179 if (connectRetryJob != null) {
180 connectRetryJob.cancel(true);
183 if (this.heartBeat != null) {
184 this.heartBeat.cancel(true);
187 cancelCheckAliveTimeoutTask();
189 if (messageSender != null && messageSender.isAlive()) {
190 messageSender.interrupt();
193 this.communication.stopCommunication();
196 public void reconnect() {
200 private synchronized void reconnect(boolean timeout) {
202 logger.debug("Keepalive timeout, attempting to reconnect to the bridge");
204 logger.debug("Connection problem, attempting to reconnect to the bridge");
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
212 public void sendCommands(List<CommandExecutionSpecification> commands) {
213 this.sendQueue.add(commands);
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();
222 if (handler != null && handler.getAddress().equals(address)) {
225 } catch (IllegalStateException e) {
226 logger.trace("Handler for id {} not initialized", address);
235 * needed with fast reconnect to update status of things
237 public void forceRefreshThings() {
238 for (Thing thing : getThing().getThings()) {
239 if (thing.getHandler() instanceof LuxomThingHandler) {
240 LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
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,
251 sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.HEARTBEAT.getCommand())));
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);
260 if (!validConfig || needsReconnect) {
265 this.config = newConfig;
267 if (needsReconnect) {
272 public void handleCommunicationError(IOException e) {
273 logger.debug("Communication error while reading, will try to reconnect. Error: {}", e.getMessage());
278 public void dispose() {
282 public void handleIncomingLuxomMessage(String luxomMessage) throws IOException {
283 cancelCheckAliveTimeoutTask(); // we got a message
285 logger.trace("Luxom: received {}", luxomMessage);
286 LuxomCommand luxomCommand = new LuxomCommand(luxomMessage);
288 // Now dispatch update to the proper thing handler
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...
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;
315 if (luxomCommand != null) {
316 LuxomThingHandler handler = findThingHandler(luxomCommand.getAddress());
318 if (handler != null) {
319 handler.handleCommandComingFromBridge(luxomCommand);
321 logger.warn("No handler found command {} for address : {}", luxomMessage,
322 luxomCommand.getAddress());
325 logger.warn("Something was wrong with the order of incoming commands, resulting command is null");
328 logger.trace("Luxom: not handled {}", luxomMessage);
330 logger.trace("nrOfPermits after receive: {}", nrOfSendPermits.get());
333 private void cancelCheckAliveTimeoutTask() {
334 var task = heartBeatTimeoutTask;
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.
342 private synchronized void cmdSystemInfo(@Nullable String info) {
343 systemInfo.setSwVersion(info);