]> git.basschouten.com Git - openhab-addons.git/blob
20fec6eefa337d7003b136c8d9fa4938d7dcfbbb
[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.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. */
65     private Optional<Integer> firmwareVersion = Optional.empty();
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, 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                         addr + " " + 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.isEmpty()) {
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.isEmpty()) {
241             logger.warn("LCN module firmware version unknown");
242             return false;
243         }
244         return firmwareVersion.map(v -> v >= LcnBindingConstants.FIRMWARE_2013).orElse(false);
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
281             if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) {
282                 return;
283             }
284
285             if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps,
286                     PckGenerator.requestLedsAndLogicOpsStatus())) {
287                 return;
288             }
289
290             if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) {
291                 return;
292             }
293
294             // Variable requests
295             firmwareVersion.ifPresent(firmwareVersion -> { // Firmware version is required
296                 // Use the chance to remove a failed "typeless variable" request
297                 if (lastRequestedVarWithoutTypeInResponse != Variable.UNKNOWN) {
298                     RequestStatus requestStatus = requestStatusVars.get(lastRequestedVarWithoutTypeInResponse);
299                     if (requestStatus != null && requestStatus.isTimeout(timeoutMSec, currTime)) {
300                         lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
301                     }
302                 }
303                 // Variables
304                 for (Map.Entry<Variable, RequestStatus> kv : this.requestStatusVars.entrySet()) {
305                     RequestStatus requestStatus = kv.getValue();
306                     try {
307                         if (requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
308                             // Detect if we can send immediately or if we have to wait for a "typeless" request first
309                             boolean hasTypeInResponse = kv.getKey().hasTypeInResponse(firmwareVersion);
310                             if (hasTypeInResponse || this.lastRequestedVarWithoutTypeInResponse == Variable.UNKNOWN) {
311                                 try {
312                                     conn.queue(this.addr, false,
313                                             PckGenerator.requestVarStatus(kv.getKey(), firmwareVersion));
314                                     requestStatus.onRequestSent(currTime);
315                                     if (!hasTypeInResponse) {
316                                         this.lastRequestedVarWithoutTypeInResponse = kv.getKey();
317                                     }
318                                     return;
319                                 } catch (LcnException ex) {
320                                     logger.warn("{}: Failed to generate PCK message: {}: {}", addr, kv.getKey(),
321                                             ex.getMessage());
322                                     requestStatus.reset();
323                                     lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
324                                 }
325                             }
326                         }
327                     } catch (LcnException e) {
328                         logger.warn("{}: Failed to receive measurement value: {}", addr, e.getMessage());
329                     }
330                 }
331             });
332
333             // Try to send next acknowledged command. Will also detect failed ones.
334             this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
335         } catch (LcnException e) {
336             logger.warn("{}: Failed to receive status message: {}", addr, e.getMessage());
337         }
338     }
339
340     /**
341      * Gets the LCN module's firmware date.
342      *
343      * @return the date
344      */
345     public Optional<Integer> getFirmwareVersion() {
346         return firmwareVersion;
347     }
348
349     /**
350      * Sets the LCN module's firmware date.
351      *
352      * @param firmwareVersion the date
353      */
354     public void setFirmwareVersion(int firmwareVersion) {
355         this.firmwareVersion = Optional.of(firmwareVersion);
356
357         requestFirmwareVersion.onResponseReceived();
358
359         // increase poll interval, if the LCN module sends status updates of a variable event-based
360         requestStatusVars.entrySet().stream().filter(e -> e.getKey().isEventBased(firmwareVersion)).forEach(e -> {
361             RequestStatus value = e.getValue();
362             value.setMaxAgeMSec(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC);
363         });
364     }
365
366     /**
367      * Updates the variable value cache.
368      *
369      * @param variable the variable to update
370      * @param value the new value
371      */
372     public void updateVariableValue(Variable variable, VariableValue value) {
373         variableValue.put(variable, value);
374     }
375
376     /**
377      * Gets the current value of a variable from the cache.
378      *
379      * @param variable the variable to retrieve the value for
380      * @return the value of the variable
381      * @throws LcnException when the variable is not in the cache
382      */
383     public long getVariableValue(Variable variable) throws LcnException {
384         return Optional.ofNullable(variableValue.get(variable)).map(v -> v.toNative(variable.useLcnSpecialValues()))
385                 .orElseThrow(() -> new LcnException("Current variable value unknown"));
386     }
387
388     /**
389      * Requests the current value of all dimmer outputs.
390      */
391     public void refreshAllOutputs() {
392         Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh);
393     }
394
395     /**
396      * Requests the current value of the given dimmer output.
397      *
398      * @param number 0..3
399      */
400     public void refreshOutput(int number) {
401         requestStatusOutputs[number].refresh();
402     }
403
404     /**
405      * Requests the current value of all relays.
406      */
407     public void refreshRelays() {
408         requestStatusRelays.refresh();
409     }
410
411     /**
412      * Requests the current value of all binary sensor.
413      */
414     public void refreshBinarySensors() {
415         requestStatusBinSensors.refresh();
416     }
417
418     /**
419      * Requests the current value of the given variable.
420      *
421      * @param variable the variable to request
422      */
423     public void refreshVariable(Variable variable) {
424         RequestStatus requestStatus = requestStatusVars.get(variable);
425         if (requestStatus != null) {
426             requestStatus.refresh();
427         }
428     }
429
430     /**
431      * Requests the current value of all LEDs and logic operations.
432      */
433     public void refreshLedsAndLogic() {
434         requestStatusLedsAndLogicOps.refresh();
435     }
436
437     /**
438      * Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB.
439      */
440     public void refreshStatusLedsAnLogicAfterChange() {
441         requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.currentTimeMillis());
442     }
443
444     /**
445      * Requests the current locking states of all keys.
446      */
447     public void refreshStatusLockedKeys() {
448         requestStatusLockedKeys.refresh();
449     }
450
451     /**
452      * Requests the current locking states of all keys, after a lock state has been changed by openHAB.
453      */
454     public void refreshStatusStatusLockedKeysAfterChange() {
455         requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.currentTimeMillis());
456     }
457
458     /**
459      * Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output
460      *
461      * @param outputId 0..3
462      */
463     public void onOutputResponseReceived(int outputId) {
464         requestStatusOutputs[outputId].onResponseReceived();
465     }
466
467     /**
468      * Resets the value request logic, when a requested value has been received from the LCN module: Relay
469      */
470     public void onRelayResponseReceived() {
471         requestStatusRelays.onResponseReceived();
472     }
473
474     /**
475      * Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor
476      */
477     public void onBinarySensorsResponseReceived() {
478         requestStatusBinSensors.onResponseReceived();
479     }
480
481     /**
482      * Resets the value request logic, when a requested value has been received from the LCN module: Variable
483      *
484      * @param variable the received variable type
485      */
486     public void onVariableResponseReceived(Variable variable) {
487         RequestStatus requestStatus = requestStatusVars.get(variable);
488         if (requestStatus != null) {
489             requestStatus.onResponseReceived();
490         }
491
492         if (variable == lastRequestedVarWithoutTypeInResponse) {
493             lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN; // Reset
494         }
495     }
496
497     /**
498      * Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic
499      */
500     public void onLedsAndLogicResponseReceived() {
501         requestStatusLedsAndLogicOps.onResponseReceived();
502     }
503
504     /**
505      * Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state
506      */
507     public void onLockedKeysResponseReceived() {
508         requestStatusLockedKeys.onResponseReceived();
509     }
510
511     /**
512      * Returns the module's bus address.
513      */
514     public LcnAddr getAddress() {
515         return addr;
516     }
517 }