]> git.basschouten.com Git - openhab-addons.git/blob
0bdeee37c18318aa24d57f9866acdd8d4ed3216e
[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.velux.internal.bridge.slip;
14
15 import java.io.Closeable;
16 import java.io.IOException;
17 import java.text.ParseException;
18 import java.util.TreeSet;
19
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;
33
34 /**
35  * SLIP-based 2nd Level I/O interface towards the <B>Velux</B> bridge.
36  * <P>
37  * It provides methods for pre- and post- communication as well as a common method for the real communication.
38  * <P>
39  * In addition to the generic {@link VeluxBridge} methods, i.e.
40  * <UL>
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>
45  * </UL>
46  * the following class access methods provides the protocol-specific implementation:
47  * <UL>
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>
51  * </UL>
52  *
53  * @author Guenther Schreiner - Initial contribution.
54  * @author Andrew Fiddian-Green - Refactored (simplified) the message processing loop
55  */
56 @NonNullByDefault
57 public class SlipVeluxBridge extends VeluxBridge implements Closeable {
58
59     private final Logger logger = LoggerFactory.getLogger(SlipVeluxBridge.class);
60
61     /*
62      * ***************************
63      * ***** Private Objects *****
64      */
65
66     /**
67      * Timeout for sequence of one request with (optional multiple) responses.
68      * <P>
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.
71      */
72     static final long COMMUNICATION_TIMEOUT_MSECS = 60000L;
73     /**
74      * Wait interval within sequence after the request and no immediate response.
75      * <P>
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.
78      */
79     static final long COMMUNICATION_RETRY_MSECS = 5000L;
80
81     /*
82      * ***************************
83      * ***** Private Objects *****
84      */
85
86     private final byte[] emptyPacket = new byte[0];
87     private Connection connection = new Connection();
88
89     /**
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}.
92      */
93     private BridgeAPI bridgeAPI;
94
95     /**
96      * Constructor.
97      * <P>
98      * Inherits the initialization of the binding-wide instance for dealing for common informations and
99      * initializes the Velux bridge connection settings.
100      *
101      * @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
102      */
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.");
110     }
111
112     /**
113      * Destructor.
114      * <P>
115      * De-initializes the binding-wide instance.
116      */
117     @Override
118     public void shutdown() {
119         logger.trace("shutdown() called.");
120         connection.resetConnection();
121         logger.trace("shutdown() done.");
122     }
123
124     /**
125      * Provides information about the base-level communication method and
126      * any kind of available gateway interactions.
127      * <P>
128      * <B>Note:</B> the implementation within this class {@link SlipVeluxBridge} as inherited from {@link VeluxBridge}
129      * will return the protocol-specific class implementations.
130      * <P>
131      * The information will be initialized by the corresponding API class {@link SlipBridgeAPI}.
132      *
133      * @return <b>bridgeAPI</b> of type {@link BridgeAPI} contains all possible methods.
134      */
135     @Override
136     public BridgeAPI bridgeAPI() {
137         logger.trace("bridgeAPI() called.");
138         return bridgeAPI;
139     }
140
141     /**
142      * <B>Method as implementation of abstract superclass method.</B>
143      * <P>
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).
147      *
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.
152      *
153      */
154     @Override
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);
159     }
160
161     /**
162      * Returns the timestamp in milliseconds since Unix epoch
163      * of last (potentially faulty) communication.
164      *
165      * @return timestamp in milliseconds.
166      */
167     @Override
168     public long lastCommunication() {
169         return connection.lastCommunication();
170     }
171
172     /**
173      * Returns the timestamp in milliseconds since Unix epoch
174      * of last successful communication.
175      *
176      * @return timestamp in milliseconds.
177      */
178     @Override
179     public long lastSuccessfulCommunication() {
180         return connection.lastSuccessfulCommunication();
181     }
182
183     /**
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).
186      *
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
189      *            definition.
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
193      */
194     private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProtocol communication,
195             boolean useAuthentication) {
196         logger.trace("bridgeDirectCommunicate() '{}', {}authenticated", communication.name(),
197                 useAuthentication ? "" : "un");
198
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;
207
208         // logger messages
209         final String logMsg = "bridgeDirectCommunicate() [{}] {} => {} {} {}";
210         final String ipAddr = bridgeInstance.veluxBridgeConfiguration().ipAddress;
211
212         if (isProtocolTraceEnabled) {
213             Threads.findDeadlocked();
214         }
215
216         logger.debug(logMsg, ipAddr, txName, "started =>", Thread.currentThread(), "");
217
218         boolean looping = false;
219         boolean success = false;
220         boolean sending = false;
221         boolean rcvonly = false;
222         byte[] txPacket = emptyPacket;
223
224         // handling of the requests
225         switch (txEnum) {
226             case GW_OPENHAB_CLOSE:
227                 logger.trace(logMsg, ipAddr, txName, "shut down command", "=> executing", "");
228                 connection.resetConnection();
229                 success = true;
230                 break;
231
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", "");
236                     looping = true;
237                 } else if (connection.isMessageAvailable()) {
238                     logger.trace(logMsg, ipAddr, txName, "message(s) waiting", "=> start reading", "");
239                     looping = true;
240                 } else {
241                     logger.trace(logMsg, ipAddr, txName, "no waiting messages", "=> done", "");
242                 }
243                 rcvonly = true;
244                 break;
245
246             default:
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", "");
251                     break;
252                 }
253                 txPacket = new SlipRFC1055().encode(slipEnc.toMessage());
254                 logger.trace(logMsg, ipAddr, txName, "command ready", "=> start sending", "");
255                 looping = sending = true;
256         }
257
258         while (looping) {
259             // timeout
260             if (System.currentTimeMillis() > expiryTime) {
261                 logger.warn(logMsg, ipAddr, txName, "process loop time out", "=> aborting", "=> PLEASE REPORT !!");
262                 // abort the processing loop
263                 break;
264             }
265
266             // send command (optionally), and receive response
267             byte[] rxPacket;
268             try {
269                 if (sending) {
270                     if (isProtocolTraceEnabled) {
271                         logger.info("sending command {}", txName);
272                     }
273                     if (logger.isTraceEnabled()) {
274                         logger.trace(logMsg, ipAddr, txName, txName, "=> sending data =>", new Packet(txData));
275                     } else {
276                         logger.debug(logMsg, ipAddr, txName, txName, "=> sending data length =>", txData.length);
277                     }
278                 }
279                 rxPacket = connection.io(this.bridgeInstance, sending ? txPacket : emptyPacket);
280                 // message sent, don't send it again
281                 sending = false;
282                 if (rxPacket.length == 0) {
283                     // only log in send mode (in receive-only mode, no response is ok)
284                     if (!rcvonly) {
285                         logger.debug(logMsg, ipAddr, txName, "no response", "=> aborting", "");
286                     }
287                     // abort the processing loop
288                     break;
289                 }
290             } catch (IOException e) {
291                 logger.debug(logMsg, ipAddr, txName, "i/o error =>", e.getMessage(), "=> aborting");
292                 // abort the processing loop
293                 break;
294             }
295
296             // RFC1055 decode response
297             byte[] rfc1055;
298             try {
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
303                 break;
304             }
305
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
311                 break;
312             }
313
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();
319
320             // logging
321             if (logger.isTraceEnabled()) {
322                 logger.trace(logMsg, ipAddr, txName, rxName, "=> received data =>", new Packet(rxData));
323             } else {
324                 logger.debug(logMsg, ipAddr, txName, rxName, "=> received data length =>", rxData.length);
325             }
326             if (isProtocolTraceEnabled) {
327                 logger.info("received message {} => {}", rxName, new Packet(rxData));
328             }
329
330             // handling of the responses
331             switch (rxEnum) {
332                 case GW_ERROR_NTF:
333                     byte code = rxData[0];
334                     switch (code) {
335                         case 7: // busy
336                             logger.trace(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> retrying");
337                             sending = true;
338                             break;
339                         case 12: // authentication failed
340                             logger.debug(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> aborting");
341                             resetAuthentication();
342                             looping = false;
343                             break;
344                         default:
345                             logger.warn(logMsg, ipAddr, txName, rxName, getErrorText(code), "=> aborting");
346                             looping = false;
347                     }
348                     break;
349
350                 case GW_NODE_INFORMATION_CHANGED_NTF:
351                 case GW_ACTIVATION_LOG_UPDATED_NTF:
352                     logger.trace(logMsg, ipAddr, txName, rxName, "=> ignorable command", "=> continuing");
353                     break;
354
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");
362                         if (rcvonly) {
363                             // receive-only: return success to confirm that product(s) were updated
364                             success = true;
365                         }
366                     }
367                     logger.trace(logMsg, ipAddr, txName, rxName, "=> special command", "=> continuing");
368                     break;
369
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");
375                         break;
376                     }
377                     logger.trace(logMsg, ipAddr, txName, rxName, "=> serialism enforced", "=> default processing");
378                     // fall through => execute default processing
379
380                 default:
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();
385             }
386
387         }
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"));
390         return success;
391     }
392
393     /**
394      * Return text description of potential GW_ERROR_NTF error codes, for logging purposes
395      *
396      * @param errCode is the GW_ERROR_NTF error code
397      * @return the description message
398      */
399     private static String getErrorText(byte errCode) {
400         switch (errCode) {
401             case 0:
402                 return "=> (0) not further defined error";
403             case 1:
404                 return "=> (1) unknown command or command is not accepted at this state";
405             case 2:
406                 return "=> (2) error on frame structure";
407             case 7:
408                 return "=> (7) busy, try again later";
409             case 8:
410                 return "=> (8) bad system table index";
411             case 12:
412                 return "=> (12) not authenticated";
413         }
414         return String.format("=> (%d) unknown error", errCode);
415     }
416
417     @Override
418     public void close() throws IOException {
419         shutdown();
420     }
421 }