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.velux.internal.bridge.slip;
15 import java.io.Closeable;
16 import java.io.IOException;
17 import java.text.ParseException;
18 import java.util.TreeSet;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.openhab.binding.velux.internal.bridge.VeluxBridge;
22 import org.openhab.binding.velux.internal.bridge.common.BridgeAPI;
23 import org.openhab.binding.velux.internal.bridge.common.BridgeCommunicationProtocol;
24 import org.openhab.binding.velux.internal.bridge.slip.io.Connection;
25 import org.openhab.binding.velux.internal.bridge.slip.utils.Packet;
26 import org.openhab.binding.velux.internal.bridge.slip.utils.SlipEncoding;
27 import org.openhab.binding.velux.internal.bridge.slip.utils.SlipRFC1055;
28 import org.openhab.binding.velux.internal.development.Threads;
29 import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
30 import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * SLIP-based 2nd Level I/O interface towards the <B>Velux</B> bridge.
37 * It provides methods for pre- and post- communication as well as a common method for the real communication.
39 * In addition to the generic {@link VeluxBridge} methods, i.e.
41 * <LI>{@link VeluxBridge#bridgeLogin} for pre-communication,</LI>
42 * <LI>{@link VeluxBridge#bridgeLogout} for post-communication,</LI>
43 * <LI>{@link VeluxBridge#bridgeCommunicate} as method for the common communication,</LI>
44 * <LI>{@link VeluxBridge#bridgeAPI} as interfacing method to all interaction prototypes,</LI>
46 * the following class access methods provides the protocol-specific implementation:
48 * <LI>{@link #bridgeDirectCommunicate} for SLIP-based communication.</LI>
49 * <LI>{@link #bridgeAPI} for return all defined protocol-specific implementations which are provided by
50 * {@link org.openhab.binding.velux.internal.bridge.slip.SlipBridgeAPI SlipBridgeAPI}.</LI>
53 * @author Guenther Schreiner - Initial contribution.
54 * @author Andrew Fiddian-Green - Refactored (simplified) the message processing loop
57 public class SlipVeluxBridge extends VeluxBridge implements Closeable {
59 private final Logger logger = LoggerFactory.getLogger(SlipVeluxBridge.class);
62 * ***************************
63 * ***** Private Objects *****
67 * Timeout for sequence of one request with (optional multiple) responses.
69 * This can only happen if there is an unexpected (undocumented) occurrence of events
70 * which has to be discussed along the Velux API documentation.
72 static final long COMMUNICATION_TIMEOUT_MSECS = 60000L;
74 * Wait interval within sequence after the request and no immediate response.
76 * This can happen if the bridge is busy due to a specific command to query and collect information from other
77 * devices via the io-homecontrol protocol.
79 static final long COMMUNICATION_RETRY_MSECS = 5000L;
82 * ***************************
83 * ***** Private Objects *****
86 private final byte[] emptyPacket = new byte[0];
87 private Connection connection = new Connection();
90 * Handler passing the interface methods to other classes.
91 * Can be accessed via method {@link org.openhab.binding.velux.internal.bridge.common.BridgeAPI BridgeAPI}.
93 private BridgeAPI bridgeAPI;
98 * Inherits the initialization of the binding-wide instance for dealing for common informations and
99 * initializes the Velux bridge connection settings.
101 * @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
103 public SlipVeluxBridge(VeluxBridgeHandler bridgeInstance) {
104 super(bridgeInstance);
105 logger.trace("SlipVeluxBridge(constructor) called.");
106 bridgeAPI = new SlipBridgeAPI(bridgeInstance);
107 supportedProtocols = new TreeSet<>();
108 supportedProtocols.add("slip");
109 logger.trace("SlipVeluxBridge(constructor) done.");
115 * De-initializes the binding-wide instance.
118 public void shutdown() {
119 logger.trace("shutdown() called.");
120 connection.resetConnection();
121 logger.trace("shutdown() done.");
125 * Provides information about the base-level communication method and
126 * any kind of available gateway interactions.
128 * <B>Note:</B> the implementation within this class {@link SlipVeluxBridge} as inherited from {@link VeluxBridge}
129 * will return the protocol-specific class implementations.
131 * The information will be initialized by the corresponding API class {@link SlipBridgeAPI}.
133 * @return <b>bridgeAPI</b> of type {@link BridgeAPI} contains all possible methods.
136 public BridgeAPI bridgeAPI() {
137 logger.trace("bridgeAPI() called.");
142 * <B>Method as implementation of abstract superclass method.</B>
144 * Initializes a client/server communication towards <b>Velux</b> veluxBridge
145 * based on the Basic I/O interface {@link Connection#io} and parameters
146 * passed as arguments (see below).
148 * @param communication Structure of interface type {@link SlipBridgeCommunicationProtocol} describing the intended
149 * communication, that is request and response interactions as well as appropriate URL definition.
150 * @param useAuthentication boolean flag to decide whether to use authenticated communication.
151 * @return <b>success</b> of type boolean which signals the success of the communication.
155 protected boolean bridgeDirectCommunicate(BridgeCommunicationProtocol communication, boolean useAuthentication) {
156 logger.trace("bridgeDirectCommunicate(BCP: {}, {}authenticated) called.", communication.name(),
157 useAuthentication ? "" : "un");
158 return bridgeDirectCommunicate((SlipBridgeCommunicationProtocol) communication, useAuthentication);
162 * Returns the timestamp in milliseconds since Unix epoch
163 * of last (potentially faulty) communication.
165 * @return timestamp in milliseconds.
168 public long lastCommunication() {
169 return connection.lastCommunication();
173 * Returns the timestamp in milliseconds since Unix epoch
174 * of last successful communication.
176 * @return timestamp in milliseconds.
179 public long lastSuccessfulCommunication() {
180 return connection.lastSuccessfulCommunication();
184 * Initializes a client/server communication towards the Velux Bridge based on the Basic I/O interface
185 * {@link Connection#io} and parameters passed as arguments (see below).
187 * @param communication a structure of interface type {@link SlipBridgeCommunicationProtocol} describing the
188 * intended communication, that is request and response interactions as well as appropriate URL
190 * @param useAuthentication a boolean flag to select whether to use authenticated communication.
191 * @return a boolean which in general signals the success of the communication, but in the
192 * special case of receive-only calls, signals if any products were updated during the call
194 private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProtocol communication,
195 boolean useAuthentication) {
196 logger.trace("bridgeDirectCommunicate() '{}', {}authenticated", communication.name(),
197 useAuthentication ? "" : "un");
199 // store common parameters as constants for frequent use
200 final short txCmd = communication.getRequestCommand().toShort();
201 final byte[] txData = communication.getRequestDataAsArrayOfBytes();
202 final Command txEnum = Command.get(txCmd);
203 final String txName = txEnum.toString();
204 final boolean isSequentialEnforced = this.bridgeInstance.veluxBridgeConfiguration().isSequentialEnforced;
205 final boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled;
206 final long expiryTime = System.currentTimeMillis() + COMMUNICATION_TIMEOUT_MSECS;
209 final String logMsg = "bridgeDirectCommunicate() [{}] {} => {} {} {}";
210 final String ipAddr = bridgeInstance.veluxBridgeConfiguration().ipAddress;
212 if (isProtocolTraceEnabled) {
213 Threads.findDeadlocked();
216 logger.debug(logMsg, ipAddr, txName, "started =>", Thread.currentThread(), "");
218 boolean looping = false;
219 boolean success = false;
220 boolean sending = false;
221 boolean rcvonly = false;
222 byte[] txPacket = emptyPacket;
224 // handling of the requests
226 case GW_OPENHAB_CLOSE:
227 logger.trace(logMsg, ipAddr, txName, "shut down command", "=> executing", "");
228 connection.resetConnection();
232 case GW_OPENHAB_RECEIVEONLY:
233 logger.trace(logMsg, ipAddr, txName, "receive-only mode", "=> checking messages", "");
234 if (!connection.isAlive()) {
235 logger.trace(logMsg, ipAddr, txName, "no connection", "=> opening", "");
237 } else if (connection.isMessageAvailable()) {
238 logger.trace(logMsg, ipAddr, txName, "message(s) waiting", "=> start reading", "");
241 logger.trace(logMsg, ipAddr, txName, "no waiting messages", "=> done", "");
247 logger.trace(logMsg, ipAddr, txName, "send mode", "=> preparing command", "");
248 SlipEncoding slipEnc = new SlipEncoding(txCmd, txData);
249 if (!slipEnc.isValid()) {
250 logger.debug(logMsg, ipAddr, txName, "slip encoding error", "=> aborting", "");
253 txPacket = new SlipRFC1055().encode(slipEnc.toMessage());
254 logger.trace(logMsg, ipAddr, txName, "command ready", "=> start sending", "");
255 looping = sending = true;
260 if (System.currentTimeMillis() > expiryTime) {
261 logger.warn(logMsg, ipAddr, txName, "process loop time out", "=> aborting", "=> PLEASE REPORT !!");
262 // abort the processing loop
266 // send command (optionally), and receive response
270 if (isProtocolTraceEnabled) {
271 logger.info("sending command {}", txName);
273 if (logger.isTraceEnabled()) {
274 logger.trace(logMsg, ipAddr, txName, txName, "=> sending data =>", new Packet(txData));
276 logger.debug(logMsg, ipAddr, txName, txName, "=> sending data length =>", txData.length);
279 rxPacket = connection.io(this.bridgeInstance, sending ? txPacket : emptyPacket);
280 // message sent, don't send it again
282 if (rxPacket.length == 0) {
283 // only log in send mode (in receive-only mode, no response is ok)
285 logger.debug(logMsg, ipAddr, txName, "no response", "=> aborting", "");
287 // abort the processing loop
290 } catch (IOException e) {
291 logger.debug(logMsg, ipAddr, txName, "i/o error =>", e.getMessage(), "=> aborting");
292 // abort the processing loop
296 // RFC1055 decode response
299 rfc1055 = new SlipRFC1055().decode(rxPacket);
300 } catch (ParseException e) {
301 logger.debug(logMsg, ipAddr, txName, "parsing error =>", e.getMessage(), "=> aborting");
302 // abort the processing loop
306 // SLIP decode response
307 SlipEncoding slipEnc = new SlipEncoding(rfc1055);
308 if (!slipEnc.isValid()) {
309 logger.debug(logMsg, ipAddr, txName, "slip decode error", "=> aborting", "");
310 // abort the processing loop
314 // attributes of the received (rx) response
315 final short rxCmd = slipEnc.getCommand();
316 final byte[] rxData = slipEnc.getData();
317 final Command rxEnum = Command.get(rxCmd);
318 final String rxName = rxEnum.toString();
321 if (logger.isTraceEnabled()) {
322 logger.trace(logMsg, ipAddr, txName, rxName, "=> received data =>", new Packet(rxData));
324 logger.debug(logMsg, ipAddr, txName, rxName, "=> received data length =>", rxData.length);
326 if (isProtocolTraceEnabled) {
327 logger.info("received message {} => {}", rxName, new Packet(rxData));
330 // handling of the responses
333 byte code = rxData[0];
336 logger.trace(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> retrying");
339 case 12: // authentication failed
340 logger.debug(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> aborting");
341 resetAuthentication();
345 logger.warn(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> aborting");
350 case GW_NODE_INFORMATION_CHANGED_NTF:
351 case GW_ACTIVATION_LOG_UPDATED_NTF:
352 logger.trace(logMsg, ipAddr, txName, rxName, "=> ignorable command", "=> continuing");
355 case GW_NODE_STATE_POSITION_CHANGED_NTF:
356 logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> starting");
357 SCgetHouseStatus receiver = new SCgetHouseStatus().setCreatorCommand(txEnum);
358 receiver.setResponse(rxCmd, rxData, isSequentialEnforced);
359 if (receiver.isCommunicationSuccessful()) {
360 bridgeInstance.existingProducts().update(receiver.getProduct());
361 logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> update submitted");
363 // receive-only: return success to confirm that product(s) were updated
367 logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> continuing");
370 case GW_COMMAND_RUN_STATUS_NTF:
371 case GW_COMMAND_REMAINING_TIME_NTF:
372 case GW_SESSION_FINISHED_NTF:
373 if (!isSequentialEnforced) {
374 logger.trace(logMsg, ipAddr, txName, rxName, "=> parallelism allowed", "=> continuing");
377 logger.trace(logMsg, ipAddr, txName, rxName, "=> serialism enforced", "=> default processing");
378 // fall through => execute default processing
381 logger.trace(logMsg, ipAddr, txName, rxName, "=> applying data length =>", rxData.length);
382 communication.setResponse(rxCmd, rxData, isSequentialEnforced);
383 looping = !communication.isCommunicationFinished();
384 success = communication.isCommunicationSuccessful();
388 // in receive-only mode 'failure` just means that no products were updated, so don't log it as a failure..
389 logger.debug(logMsg, ipAddr, txName, "finished", "=>", ((success || rcvonly) ? "success" : "failure"));
394 * Return text description of potential GW_ERROR_NTF error codes, for logging purposes
396 * @param errCode is the GW_ERROR_NTF error code
397 * @return the description message
399 private static String getErrorText(byte errCode) {
402 return "=> (0) not further defined error";
404 return "=> (1) unknown command or command is not accepted at this state";
406 return "=> (2) error on frame structure";
408 return "=> (7) busy, try again later";
410 return "=> (8) bad system table index";
412 return "=> (12) not authenticated";
414 return String.format("=> (%d) unknown error", errCode);
418 public void close() throws IOException {