]> git.basschouten.com Git - openhab-addons.git/blob
4243c731698f8a2e8969a5a1210c78f8dcce40d5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * SLIP-based 2nd Level I/O interface towards the <B>Velux</B> bridge.
37  * <P>
38  * It provides methods for pre- and post- communication as well as a common method for the real communication.
39  * <P>
40  * In addition to the generic {@link VeluxBridge} methods, i.e.
41  * <UL>
42  * <LI>{@link VeluxBridge#bridgeLogin} for pre-communication,</LI>
43  * <LI>{@link VeluxBridge#bridgeLogout} for post-communication,</LI>
44  * <LI>{@link VeluxBridge#bridgeCommunicate} as method for the common communication,</LI>
45  * <LI>{@link VeluxBridge#bridgeAPI} as interfacing method to all interaction prototypes,</LI>
46  * </UL>
47  * the following class access methods provides the protocol-specific implementation:
48  * <UL>
49  * <LI>{@link #bridgeDirectCommunicate} for SLIP-based communication.</LI>
50  * <LI>{@link #bridgeAPI} for return all defined protocol-specific implementations which are provided by
51  * {@link org.openhab.binding.velux.internal.bridge.slip.SlipBridgeAPI SlipBridgeAPI}.</LI>
52  * </UL>
53  *
54  * @author Guenther Schreiner - Initial contribution.
55  * @author Andrew Fiddian-Green - Refactored (simplified) the message processing loop
56  */
57 @NonNullByDefault
58 public class SlipVeluxBridge extends VeluxBridge implements Closeable {
59
60     private final Logger logger = LoggerFactory.getLogger(SlipVeluxBridge.class);
61
62     /*
63      * ***************************
64      * ***** Private Objects *****
65      */
66
67     /**
68      * Timeout for sequence of one request with (optional multiple) responses.
69      * <P>
70      * This can only happen if there is an unexpected (undocumented) occurrence of events
71      * which has to be discussed along the Velux API documentation.
72      */
73     static final long COMMUNICATION_TIMEOUT_MSECS = 60000L;
74     /**
75      * Wait interval within sequence after the request and no immediate response.
76      * <P>
77      * This can happen if the bridge is busy due to a specific command to query and collect information from other
78      * devices via the io-homecontrol protocol.
79      */
80     static final long COMMUNICATION_RETRY_MSECS = 5000L;
81
82     /*
83      * ***************************
84      * ***** Private Objects *****
85      */
86
87     private final byte[] emptyPacket = new byte[0];
88     private Connection connection = new Connection();
89
90     /**
91      * Handler passing the interface methods to other classes.
92      * Can be accessed via method {@link org.openhab.binding.velux.internal.bridge.common.BridgeAPI BridgeAPI}.
93      */
94     private BridgeAPI bridgeAPI;
95
96     /**
97      * Constructor.
98      * <P>
99      * Inherits the initialization of the binding-wide instance for dealing for common informations and
100      * initializes the Velux bridge connection settings.
101      *
102      * @param bridgeInstance refers to the binding-wide instance for dealing for common informations.
103      */
104     public SlipVeluxBridge(VeluxBridgeHandler bridgeInstance) {
105         super(bridgeInstance);
106         logger.trace("SlipVeluxBridge(constructor) called.");
107         bridgeAPI = new SlipBridgeAPI(bridgeInstance);
108         supportedProtocols = new TreeSet<>();
109         supportedProtocols.add("slip");
110         logger.trace("SlipVeluxBridge(constructor) done.");
111     }
112
113     /**
114      * Destructor.
115      * <P>
116      * De-initializes the binding-wide instance.
117      */
118     @Override
119     public void shutdown() {
120         logger.trace("shutdown() called.");
121         connection.resetConnection();
122         logger.trace("shutdown() done.");
123     }
124
125     /**
126      * Provides information about the base-level communication method and
127      * any kind of available gateway interactions.
128      * <P>
129      * <B>Note:</B> the implementation within this class {@link SlipVeluxBridge} as inherited from {@link VeluxBridge}
130      * will return the protocol-specific class implementations.
131      * <P>
132      * The information will be initialized by the corresponding API class {@link SlipBridgeAPI}.
133      *
134      * @return <b>bridgeAPI</b> of type {@link BridgeAPI} contains all possible methods.
135      */
136     @Override
137     public BridgeAPI bridgeAPI() {
138         logger.trace("bridgeAPI() called.");
139         return bridgeAPI;
140     }
141
142     /**
143      * <B>Method as implementation of abstract superclass method.</B>
144      * <P>
145      * Initializes a client/server communication towards <b>Velux</b> veluxBridge
146      * based on the Basic I/O interface {@link Connection#io} and parameters
147      * passed as arguments (see below).
148      *
149      * @param communication Structure of interface type {@link SlipBridgeCommunicationProtocol} describing the intended
150      *            communication, that is request and response interactions as well as appropriate URL definition.
151      * @param useAuthentication boolean flag to decide whether to use authenticated communication.
152      * @return <b>success</b> of type boolean which signals the success of the communication.
153      *
154      */
155     @Override
156     protected boolean bridgeDirectCommunicate(BridgeCommunicationProtocol communication, boolean useAuthentication) {
157         logger.trace("bridgeDirectCommunicate(BCP: {}, {}authenticated) called.", communication.name(),
158                 useAuthentication ? "" : "un");
159         return bridgeDirectCommunicate((SlipBridgeCommunicationProtocol) communication, useAuthentication);
160     }
161
162     /**
163      * Returns the timestamp in milliseconds since Unix epoch
164      * of last (potentially faulty) communication.
165      *
166      * @return timestamp in milliseconds.
167      */
168     @Override
169     public long lastCommunication() {
170         return connection.lastCommunication();
171     }
172
173     /**
174      * Returns the timestamp in milliseconds since Unix epoch
175      * of last successful communication.
176      *
177      * @return timestamp in milliseconds.
178      */
179     @Override
180     public long lastSuccessfulCommunication() {
181         return connection.lastSuccessfulCommunication();
182     }
183
184     /**
185      * Initializes a client/server communication towards the Velux Bridge based on the Basic I/O interface
186      * {@link Connection#io} and parameters passed as arguments (see below).
187      *
188      * @param communication a structure of interface type {@link SlipBridgeCommunicationProtocol} describing the
189      *            intended communication, that is request and response interactions as well as appropriate URL
190      *            definition.
191      * @param useAuthentication a boolean flag to select whether to use authenticated communication.
192      * @return a boolean which in general signals the success of the communication, but in the
193      *         special case of receive-only calls, signals if any products were updated during the call
194      */
195     private synchronized boolean bridgeDirectCommunicate(SlipBridgeCommunicationProtocol communication,
196             boolean useAuthentication) {
197         logger.trace("bridgeDirectCommunicate() '{}', {}authenticated", communication.name(),
198                 useAuthentication ? "" : "un");
199
200         // store common parameters as constants for frequent use
201         final short txCmd = communication.getRequestCommand().toShort();
202         final byte[] txData = communication.getRequestDataAsArrayOfBytes();
203         final Command txEnum = Command.get(txCmd);
204         final String txName = txEnum.toString();
205         final boolean isSequentialEnforced = this.bridgeInstance.veluxBridgeConfiguration().isSequentialEnforced;
206         final boolean isProtocolTraceEnabled = this.bridgeInstance.veluxBridgeConfiguration().isProtocolTraceEnabled;
207         final long expiryTime = System.currentTimeMillis() + COMMUNICATION_TIMEOUT_MSECS;
208
209         // logger format string
210         final String loggerFmt = String.format("bridgeDirectCommunicate() [%s] %s => {} {} {}",
211                 this.bridgeInstance.veluxBridgeConfiguration().ipAddress, txName);
212
213         if (isProtocolTraceEnabled) {
214             Threads.findDeadlocked();
215         }
216
217         logger.debug(loggerFmt, "started =>", Thread.currentThread(), "");
218
219         boolean looping = false;
220         boolean success = false;
221         boolean sending = false;
222         boolean rcvonly = false;
223         byte[] txPacket = emptyPacket;
224
225         // handling of the requests
226         switch (txEnum) {
227             case GW_OPENHAB_CLOSE:
228                 logger.trace(loggerFmt, "shut down command", "=> executing", "");
229                 connection.resetConnection();
230                 success = true;
231                 break;
232
233             case GW_OPENHAB_RECEIVEONLY:
234                 logger.trace(loggerFmt, "receive-only mode", "=> checking messages", "");
235                 if (!connection.isAlive()) {
236                     logger.trace(loggerFmt, "no connection", "=> opening", "");
237                     looping = true;
238                 } else if (connection.isMessageAvailable()) {
239                     logger.trace(loggerFmt, "message(s) waiting", "=> start reading", "");
240                     looping = true;
241                 } else {
242                     logger.trace(loggerFmt, "no waiting messages", "=> done", "");
243                 }
244                 rcvonly = true;
245                 break;
246
247             default:
248                 logger.trace(loggerFmt, "send mode", "=> preparing command", "");
249                 SlipEncoding slipEnc = new SlipEncoding(txCmd, txData);
250                 if (!slipEnc.isValid()) {
251                     logger.debug(loggerFmt, "slip encoding error", "=> aborting", "");
252                     break;
253                 }
254                 txPacket = new SlipRFC1055().encode(slipEnc.toMessage());
255                 logger.trace(loggerFmt, "command ready", "=> start sending", "");
256                 looping = sending = true;
257         }
258
259         while (looping) {
260             // timeout
261             if (System.currentTimeMillis() > expiryTime) {
262                 logger.warn(loggerFmt, "process loop time out", "=> aborting", "=> PLEASE REPORT !!");
263                 // abort the processing loop
264                 break;
265             }
266
267             // send command (optionally), and receive response
268             byte[] rxPacket;
269             try {
270                 if (sending) {
271                     if (isProtocolTraceEnabled) {
272                         logger.info("sending command {}", txName);
273                     }
274                     if (logger.isTraceEnabled()) {
275                         logger.trace(loggerFmt, txName, "=> sending data =>", new Packet(txData));
276                     } else {
277                         logger.debug(loggerFmt, txName, "=> sending data length =>", txData.length);
278                     }
279                 }
280                 rxPacket = connection.io(this.bridgeInstance, sending ? txPacket : emptyPacket);
281                 // message sent, don't send it again
282                 sending = false;
283                 if (rxPacket.length == 0) {
284                     // only log in send mode (in receive-only mode, no response is ok)
285                     if (!rcvonly) {
286                         logger.debug(loggerFmt, "no response", "=> aborting", "");
287                     }
288                     // abort the processing loop
289                     break;
290                 }
291             } catch (IOException e) {
292                 logger.debug(loggerFmt, "i/o error =>", e.getMessage(), "=> aborting");
293                 // abort the processing loop
294                 break;
295             }
296
297             // RFC1055 decode response
298             byte[] rfc1055;
299             try {
300                 rfc1055 = new SlipRFC1055().decode(rxPacket);
301             } catch (ParseException e) {
302                 logger.debug(loggerFmt, "parsing error =>", e.getMessage(), "=> aborting");
303                 // abort the processing loop
304                 break;
305             }
306
307             // SLIP decode response
308             SlipEncoding slipEnc = new SlipEncoding(rfc1055);
309             if (!slipEnc.isValid()) {
310                 logger.debug(loggerFmt, "slip decode error", "=> aborting", "");
311                 // abort the processing loop
312                 break;
313             }
314
315             // attributes of the received (rx) response
316             final short rxCmd = slipEnc.getCommand();
317             final byte[] rxData = slipEnc.getData();
318             final Command rxEnum = Command.get(rxCmd);
319             final String rxName = rxEnum.toString();
320
321             // logging
322             if (logger.isTraceEnabled()) {
323                 logger.trace(loggerFmt, rxName, "=> received data =>", new Packet(rxData));
324             } else {
325                 logger.debug(loggerFmt, rxName, "=> received data length =>", rxData.length);
326             }
327             if (isProtocolTraceEnabled) {
328                 logger.info("received message {} => {}", rxName, new Packet(rxData));
329             }
330
331             // handling of the responses
332             switch (rxEnum) {
333                 case GW_ERROR_NTF:
334                     byte code = rxData[0];
335                     switch (code) {
336                         case 7: // busy
337                             logger.trace(loggerFmt, rxName, getErrorText(code), "=> retrying");
338                             sending = true;
339                             break;
340                         case 12: // authentication failed
341                             logger.debug(loggerFmt, rxName, getErrorText(code), "=> aborting");
342                             resetAuthentication();
343                             looping = false;
344                             break;
345                         default:
346                             logger.warn(loggerFmt, rxName, getErrorText(code), "=> aborting");
347                             looping = false;
348                     }
349                     break;
350
351                 case GW_NODE_INFORMATION_CHANGED_NTF:
352                 case GW_ACTIVATION_LOG_UPDATED_NTF:
353                     logger.trace(loggerFmt, rxName, "=> ignorable command", "=> continuing");
354                     break;
355
356                 case GW_NODE_STATE_POSITION_CHANGED_NTF:
357                     logger.trace(loggerFmt, rxName, "=> special command", "=> starting");
358                     SCgetHouseStatus receiver = new SCgetHouseStatus();
359                     receiver.setResponse(rxCmd, rxData, isSequentialEnforced);
360                     if (receiver.isCommunicationSuccessful()) {
361                         bridgeInstance.existingProducts().update(new ProductBridgeIndex(receiver.getNtfNodeID()),
362                                 receiver.getNtfState(), receiver.getNtfCurrentPosition(), receiver.getNtfTarget());
363                         logger.trace(loggerFmt, rxName, "=> special command", "=> product updated");
364                         if (rcvonly) {
365                             // receive-only: return success to confirm that product(s) were updated
366                             success = true;
367                         }
368                     }
369                     logger.trace(loggerFmt, rxName, "=> special command", "=> continuing");
370                     break;
371
372                 case GW_COMMAND_RUN_STATUS_NTF:
373                 case GW_COMMAND_REMAINING_TIME_NTF:
374                 case GW_SESSION_FINISHED_NTF:
375                     if (!isSequentialEnforced) {
376                         logger.trace(loggerFmt, rxName, "=> parallelism allowed", "=> continuing");
377                         break;
378                     }
379                     logger.trace(loggerFmt, rxName, "=> serialism enforced", "=> default processing");
380                     // fall through => execute default processing
381
382                 default:
383                     logger.trace(loggerFmt, rxName, "=> applying data length =>", rxData.length);
384                     communication.setResponse(rxCmd, rxData, isSequentialEnforced);
385                     looping = !communication.isCommunicationFinished();
386                     success = communication.isCommunicationSuccessful();
387             }
388
389         }
390         // in receive-only mode 'failure` just means that no products were updated, so don't log it as a failure..
391         logger.debug(loggerFmt, "finished", "=>", ((success || rcvonly) ? "success" : "failure"));
392         return success;
393     }
394
395     /**
396      * Return text description of potential GW_ERROR_NTF error codes, for logging purposes
397      *
398      * @param errCode is the GW_ERROR_NTF error code
399      * @return the description message
400      */
401     private static String getErrorText(byte errCode) {
402         switch (errCode) {
403             case 0:
404                 return "=> (0) not further defined error";
405             case 1:
406                 return "=> (1) unknown command or command is not accepted at this state";
407             case 2:
408                 return "=> (2) error on frame structure";
409             case 7:
410                 return "=> (7) busy, try again later";
411             case 8:
412                 return "=> (8) bad system table index";
413             case 12:
414                 return "=> (12) not authenticated";
415         }
416         return String.format("=> (%d) unknown error", errCode);
417     }
418
419     @Override
420     public void close() throws IOException {
421         shutdown();
422     }
423 }