]> git.basschouten.com Git - openhab-addons.git/blob
a7799240c2e3f899151e2394777f11ad78353f58
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.lcn.internal.connection;
14
15 import java.util.Arrays;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.Optional;
19 import java.util.Queue;
20 import java.util.concurrent.ConcurrentLinkedQueue;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.lcn.internal.LcnBindingConstants;
25 import org.openhab.binding.lcn.internal.common.LcnAddr;
26 import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
27 import org.openhab.binding.lcn.internal.common.LcnDefs;
28 import org.openhab.binding.lcn.internal.common.LcnException;
29 import org.openhab.binding.lcn.internal.common.PckGenerator;
30 import org.openhab.binding.lcn.internal.common.Variable;
31 import org.openhab.binding.lcn.internal.common.VariableValue;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Holds data of an LCN module.
37  * <ul>
38  * <li>Stores the module's firmware version (if requested)
39  * <li>Manages the scheduling of status-requests
40  * <li>Manages the scheduling of acknowledged commands
41  * </ul>
42  *
43  * @author Tobias Jüttner - Initial Contribution
44  * @author Fabian Wolter - Migration to OH2
45  */
46 @NonNullByDefault
47 public class ModInfo {
48     private final Logger logger = LoggerFactory.getLogger(ModInfo.class);
49     /** Total number of request to sent before going into failed-state. */
50     private static final int NUM_TRIES = 3;
51
52     /** Poll interval for status values that automatically send their values on change. */
53     private static final int MAX_STATUS_EVENTBASED_VALUEAGE_MSEC = 600000;
54
55     /** Poll interval for status values that do not send their values on change (always polled). */
56     private static final int MAX_STATUS_POLLED_VALUEAGE_MSEC = 30000;
57
58     /** Status request delay after a command has been send which potentially changed that status. */
59     private static final int STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC = 2000;
60
61     /** The LCN module's address. */
62     private final LcnAddr addr;
63
64     /** Firmware date of the LCN module. -1 means "unknown". */
65     private int firmwareVersion = -1;
66
67     /** Firmware version request status. */
68     private final RequestStatus requestFirmwareVersion = new RequestStatus(-1, NUM_TRIES, "Firmware Version");
69
70     /** Output-port request status (0..3). */
71     private final RequestStatus[] requestStatusOutputs = new RequestStatus[LcnChannelGroup.OUTPUT.getCount()];
72
73     /** Relays request status (all 8). */
74     private final RequestStatus requestStatusRelays = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
75             "Relays");
76
77     /** Binary-sensors request status (all 8). */
78     private final RequestStatus requestStatusBinSensors = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC,
79             NUM_TRIES, "Binary Sensors");
80
81     /**
82      * Variables request status.
83      * Lazy initialization: Will be filled once the firmware version is known.
84      */
85     private final Map<Variable, @Nullable RequestStatus> requestStatusVars = new HashMap<>();
86
87     /**
88      * Caches the values of the variables, needed for changing the values.
89      */
90     private final Map<Variable, VariableValue> variableValue = new HashMap<>();
91
92     /** LEDs and logic-operations request status (all 12+4). */
93     private final RequestStatus requestStatusLedsAndLogicOps = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC,
94             NUM_TRIES, "LEDs and Logic");
95
96     /** Key lock-states request status (all tables, A-D). */
97     private final RequestStatus requestStatusLockedKeys = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
98             "Key Locks");
99
100     /**
101      * Holds the last LCN variable requested whose response will not contain the variable's type.
102      * {@link Variable#UNKNOWN} means there is currently no such request.
103      */
104     private Variable lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
105
106     /**
107      * List of queued PCK commands to be acknowledged by the LCN module.
108      * Commands are always without address header.
109      * Note that the first one might currently be "in progress".
110      */
111     private final Queue<byte @Nullable []> pckCommandsWithAck = new ConcurrentLinkedQueue<>();
112
113     /** Status data for the currently processed {@link PckCommandWithAck}. */
114     private final RequestStatus requestCurrentPckCommandWithAck = new RequestStatus(-1, NUM_TRIES, "Commands with Ack");
115
116     /**
117      * Constructor.
118      *
119      * @param addr the module's address
120      */
121     public ModInfo(LcnAddr addr) {
122         this.addr = addr;
123         for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
124             requestStatusOutputs[i] = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
125                     "Output " + (i + 1));
126         }
127
128         for (Variable var : Variable.values()) {
129             if (var != Variable.UNKNOWN) {
130                 this.requestStatusVars.put(var, new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
131                         var.getType() + " " + (var.getNumber() + 1)));
132             }
133         }
134     }
135
136     /**
137      * Gets the last requested variable whose response will not contain the variables type.
138      *
139      * @return the "typeless" variable
140      */
141     public Variable getLastRequestedVarWithoutTypeInResponse() {
142         return this.lastRequestedVarWithoutTypeInResponse;
143     }
144
145     /**
146      * Sets the last requested variable whose response will not contain the variables type.
147      *
148      * @param var the "typeless" variable
149      */
150     public void setLastRequestedVarWithoutTypeInResponse(Variable var) {
151         this.lastRequestedVarWithoutTypeInResponse = var;
152     }
153
154     /**
155      * Queues a PCK command to be sent.
156      * It will request an acknowledge from the LCN module on receipt.
157      * If there is no response within the request timeout, the command is retried.
158      *
159      * @param data the PCK command to send (without address header)
160      * @param timeoutMSec the time to wait for a response before retrying a request
161      * @param currTime the current time stamp
162      */
163     public void queuePckCommandWithAck(byte[] data, Connection conn, long timeoutMSec, long currTime) {
164         this.pckCommandsWithAck.add(data);
165         // Try to process the new acknowledged command. Will do nothing if another one is still in progress.
166         this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
167     }
168
169     /**
170      * Called whenever an acknowledge is received from the LCN module.
171      *
172      * @param code the LCN internal code. -1 means "positive" acknowledge
173      * @param timeoutMSec the time to wait for a response before retrying a request
174      * @param currTime the current time stamp
175      */
176     public void onAck(int code, Connection conn, long timeoutMSec, long currTime) {
177         if (this.requestCurrentPckCommandWithAck.isActive()) { // Check if we wait for an ack.
178             this.pckCommandsWithAck.poll();
179             this.requestCurrentPckCommandWithAck.reset();
180             // Try to process next acknowledged command
181             this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
182         }
183     }
184
185     /**
186      * Sends the next acknowledged command from the queue.
187      *
188      * @param conn the {@link Connection} belonging to this {@link ModInfo}
189      * @param timeoutMSec the time to wait for a response before retrying a request
190      * @param currTime the current time stamp
191      * @return true if a new command was sent
192      * @throws LcnException when a command response timed out
193      */
194     private boolean tryProcessNextCommandWithAck(Connection conn, long timeoutMSec, long currTime) {
195         // Use the chance to remove a failed command first
196         if (this.requestCurrentPckCommandWithAck.isFailed(timeoutMSec, currTime)) {
197             byte[] failedCommand = this.pckCommandsWithAck.poll();
198             this.requestCurrentPckCommandWithAck.reset();
199
200             if (failedCommand != null) {
201                 logger.warn("{}: Module did not respond to command: {}", addr,
202                         new String(failedCommand, LcnDefs.LCN_ENCODING));
203             }
204         }
205         // Peek new command
206         if (!this.pckCommandsWithAck.isEmpty() && !this.requestCurrentPckCommandWithAck.isActive()) {
207             this.requestCurrentPckCommandWithAck.nextRequestIn(0, currTime);
208         }
209         byte[] command = this.pckCommandsWithAck.peek();
210         if (command == null) {
211             return false;
212         }
213         try {
214             if (requestCurrentPckCommandWithAck.shouldSendNextRequest(timeoutMSec, currTime)) {
215                 conn.queueAndSend(new SendDataPck(addr, true, command));
216                 this.requestCurrentPckCommandWithAck.onRequestSent(currTime);
217             }
218         } catch (LcnException e) {
219             logger.warn("{}: Could not send command: {}: {}", addr, new String(command, LcnDefs.LCN_ENCODING),
220                     e.getMessage());
221         }
222         return true;
223     }
224
225     /**
226      * Triggers a request to retrieve the firmware version of the LCN module, if it is not known, yet.
227      */
228     public void requestFirmwareVersion() {
229         if (firmwareVersion == -1) {
230             requestFirmwareVersion.refresh();
231         }
232     }
233
234     /**
235      * Used to check if the module has the measurement processing firmware (since Feb. 2013).
236      *
237      * @return if the module has at least 4 threshold registers and 12 variables
238      */
239     public boolean hasExtendedMeasurementProcessing() {
240         if (firmwareVersion == -1) {
241             logger.warn("LCN module firmware version unknown");
242             return false;
243         }
244         return firmwareVersion >= LcnBindingConstants.FIRMWARE_2013;
245     }
246
247     private boolean update(Connection conn, long timeoutMSec, long currTime, RequestStatus requestStatus, String pck)
248             throws LcnException {
249         if (requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
250             conn.queue(this.addr, false, pck);
251             requestStatus.onRequestSent(currTime);
252             return true;
253         }
254         return false;
255     }
256
257     /**
258      * Keeps the request logic active.
259      * Must be called periodically.
260      *
261      * @param conn the {@link Connection} belonging to this {@link ModInfo}
262      * @param timeoutMSec the time to wait for a response before retrying a request
263      * @param currTime the current time stamp
264      */
265     void update(Connection conn, long timeoutMSec, long currTime) {
266         try {
267             if (update(conn, timeoutMSec, currTime, requestFirmwareVersion, PckGenerator.requestSn())) {
268                 return;
269             }
270
271             for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
272                 if (update(conn, timeoutMSec, currTime, requestStatusOutputs[i], PckGenerator.requestOutputStatus(i))) {
273                     return;
274                 }
275             }
276
277             if (update(conn, timeoutMSec, currTime, requestStatusRelays, PckGenerator.requestRelaysStatus())) {
278                 return;
279             }
280             if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) {
281                 return;
282             }
283
284             // Variable requests
285             if (this.firmwareVersion != -1) { // Firmware version is required
286                 // Use the chance to remove a failed "typeless variable" request
287                 if (lastRequestedVarWithoutTypeInResponse != Variable.UNKNOWN) {
288                     RequestStatus requestStatus = requestStatusVars.get(lastRequestedVarWithoutTypeInResponse);
289                     if (requestStatus != null && requestStatus.isTimeout(timeoutMSec, currTime)) {
290                         lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
291                     }
292                 }
293                 // Variables
294                 for (Map.Entry<Variable, @Nullable RequestStatus> kv : this.requestStatusVars.entrySet()) {
295                     RequestStatus requestStatus = kv.getValue();
296                     if (requestStatus != null && requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
297                         // Detect if we can send immediately or if we have to wait for a "typeless" request first
298                         boolean hasTypeInResponse = kv.getKey().hasTypeInResponse(this.firmwareVersion);
299                         if (hasTypeInResponse || this.lastRequestedVarWithoutTypeInResponse == Variable.UNKNOWN) {
300                             try {
301                                 conn.queue(this.addr, false,
302                                         PckGenerator.requestVarStatus(kv.getKey(), this.firmwareVersion));
303                                 requestStatus.onRequestSent(currTime);
304                                 if (!hasTypeInResponse) {
305                                     this.lastRequestedVarWithoutTypeInResponse = kv.getKey();
306                                 }
307                                 return;
308                             } catch (LcnException ex) {
309                                 requestStatus.reset();
310                             }
311                         }
312                     }
313                 }
314             }
315
316             if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps,
317                     PckGenerator.requestLedsAndLogicOpsStatus())) {
318                 return;
319             }
320
321             if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) {
322                 return;
323             }
324
325             // Try to send next acknowledged command. Will also detect failed ones.
326             this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
327         } catch (LcnException e) {
328             logger.warn("{}: Failed to receive status message: {}", addr, e.getMessage());
329         }
330     }
331
332     /**
333      * Gets the LCN module's firmware date.
334      *
335      * @return the date
336      */
337     public int getFirmwareVersion() {
338         return this.firmwareVersion;
339     }
340
341     /**
342      * Sets the LCN module's firmware date.
343      *
344      * @param firmwareVersion the date
345      */
346     public void setFirmwareVersion(int firmwareVersion) {
347         this.firmwareVersion = firmwareVersion;
348
349         requestFirmwareVersion.onResponseReceived();
350
351         // increase poll interval, if the LCN module sends status updates of a variable event-based
352         requestStatusVars.entrySet().stream().filter(e -> e.getKey().isEventBased(firmwareVersion)).forEach(e -> {
353             RequestStatus value = e.getValue();
354             if (value != null) {
355                 value.setMaxAgeMSec(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC);
356             }
357         });
358     }
359
360     /**
361      * Updates the variable value cache.
362      *
363      * @param variable the variable to update
364      * @param value the new value
365      */
366     public void updateVariableValue(Variable variable, VariableValue value) {
367         variableValue.put(variable, value);
368     }
369
370     /**
371      * Gets the current value of a variable from the cache.
372      *
373      * @param variable the variable to retrieve the value for
374      * @return the value of the variable
375      * @throws LcnException when the variable is not in the cache
376      */
377     public long getVariableValue(Variable variable) throws LcnException {
378         return Optional.ofNullable(variableValue.get(variable)).map(v -> v.toNative(variable.useLcnSpecialValues()))
379                 .orElseThrow(() -> new LcnException("Current variable value unknown"));
380     }
381
382     /**
383      * Requests the current value of all dimmer outputs.
384      */
385     public void refreshAllOutputs() {
386         Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh);
387     }
388
389     /**
390      * Requests the current value of the given dimmer output.
391      *
392      * @param number 0..3
393      */
394     public void refreshOutput(int number) {
395         requestStatusOutputs[number].refresh();
396     }
397
398     /**
399      * Requests the current value of all relays.
400      */
401     public void refreshRelays() {
402         requestStatusRelays.refresh();
403     }
404
405     /**
406      * Requests the current value of all binary sensor.
407      */
408     public void refreshBinarySensors() {
409         requestStatusBinSensors.refresh();
410     }
411
412     /**
413      * Requests the current value of the given variable.
414      *
415      * @param variable the variable to request
416      */
417     public void refreshVariable(Variable variable) {
418         RequestStatus requestStatus = requestStatusVars.get(variable);
419         if (requestStatus != null) {
420             requestStatus.refresh();
421         }
422     }
423
424     /**
425      * Requests the current value of all LEDs and logic operations.
426      */
427     public void refreshLedsAndLogic() {
428         requestStatusLedsAndLogicOps.refresh();
429     }
430
431     /**
432      * Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB.
433      */
434     public void refreshStatusLedsAnLogicAfterChange() {
435         requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
436     }
437
438     /**
439      * Requests the current locking states of all keys.
440      */
441     public void refreshStatusLockedKeys() {
442         requestStatusLockedKeys.refresh();
443     }
444
445     /**
446      * Requests the current locking states of all keys, after a lock state has been changed by openHAB.
447      */
448     public void refreshStatusStatusLockedKeysAfterChange() {
449         requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
450     }
451
452     /**
453      * Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output
454      *
455      * @param outputId 0..3
456      */
457     public void onOutputResponseReceived(int outputId) {
458         requestStatusOutputs[outputId].onResponseReceived();
459     }
460
461     /**
462      * Resets the value request logic, when a requested value has been received from the LCN module: Relay
463      */
464     public void onRelayResponseReceived() {
465         requestStatusRelays.onResponseReceived();
466     }
467
468     /**
469      * Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor
470      */
471     public void onBinarySensorsResponseReceived() {
472         requestStatusBinSensors.onResponseReceived();
473     }
474
475     /**
476      * Resets the value request logic, when a requested value has been received from the LCN module: Variable
477      *
478      * @param variable the received variable type
479      */
480     public void onVariableResponseReceived(Variable variable) {
481         RequestStatus requestStatus = requestStatusVars.get(variable);
482         if (requestStatus != null) {
483             requestStatus.onResponseReceived();
484         }
485     }
486
487     /**
488      * Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic
489      */
490     public void onLedsAndLogicResponseReceived() {
491         requestStatusLedsAndLogicOps.onResponseReceived();
492     }
493
494     /**
495      * Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state
496      */
497     public void onLockedKeysResponseReceived() {
498         requestStatusLockedKeys.onResponseReceived();
499     }
500 }