2 * Copyright (c) 2010-2021 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.lcn.internal.connection;
15 import java.util.Arrays;
16 import java.util.HashMap;
18 import java.util.Optional;
19 import java.util.Queue;
20 import java.util.concurrent.ConcurrentLinkedQueue;
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;
36 * Holds data of an LCN module.
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
43 * @author Tobias Jüttner - Initial Contribution
44 * @author Fabian Wolter - Migration to OH2
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;
52 /** Poll interval for status values that automatically send their values on change. */
53 private static final int MAX_STATUS_EVENTBASED_VALUEAGE_MSEC = 600000;
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;
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;
61 /** The LCN module's address. */
62 private final LcnAddr addr;
64 /** Firmware date of the LCN module. -1 means "unknown". */
65 private int firmwareVersion = -1;
67 /** Firmware version request status. */
68 private final RequestStatus requestFirmwareVersion = new RequestStatus(-1, NUM_TRIES, "Firmware Version");
70 /** Output-port request status (0..3). */
71 private final RequestStatus[] requestStatusOutputs = new RequestStatus[LcnChannelGroup.OUTPUT.getCount()];
73 /** Relays request status (all 8). */
74 private final RequestStatus requestStatusRelays = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
77 /** Binary-sensors request status (all 8). */
78 private final RequestStatus requestStatusBinSensors = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC,
79 NUM_TRIES, "Binary Sensors");
82 * Variables request status.
83 * Lazy initialization: Will be filled once the firmware version is known.
85 private final Map<Variable, RequestStatus> requestStatusVars = new HashMap<>();
88 * Caches the values of the variables, needed for changing the values.
90 private final Map<Variable, VariableValue> variableValue = new HashMap<>();
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");
96 /** Key lock-states request status (all tables, A-D). */
97 private final RequestStatus requestStatusLockedKeys = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
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.
104 private Variable lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
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".
111 private final Queue<byte @Nullable []> pckCommandsWithAck = new ConcurrentLinkedQueue<>();
113 /** Status data for the currently processed {@link PckCommandWithAck}. */
114 private final RequestStatus requestCurrentPckCommandWithAck = new RequestStatus(-1, NUM_TRIES, "Commands with Ack");
119 * @param addr the module's address
121 public ModInfo(LcnAddr 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));
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)));
137 * Gets the last requested variable whose response will not contain the variables type.
139 * @return the "typeless" variable
141 public Variable getLastRequestedVarWithoutTypeInResponse() {
142 return this.lastRequestedVarWithoutTypeInResponse;
146 * Sets the last requested variable whose response will not contain the variables type.
148 * @param var the "typeless" variable
150 public void setLastRequestedVarWithoutTypeInResponse(Variable var) {
151 this.lastRequestedVarWithoutTypeInResponse = var;
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.
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
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);
170 * Called whenever an acknowledge is received from the LCN module.
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
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);
186 * Sends the next acknowledged command from the queue.
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
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();
200 if (failedCommand != null) {
201 logger.warn("{}: Module did not respond to command: {}", addr,
202 new String(failedCommand, LcnDefs.LCN_ENCODING));
206 if (!this.pckCommandsWithAck.isEmpty() && !this.requestCurrentPckCommandWithAck.isActive()) {
207 this.requestCurrentPckCommandWithAck.nextRequestIn(0, currTime);
209 byte[] command = this.pckCommandsWithAck.peek();
210 if (command == null) {
214 if (requestCurrentPckCommandWithAck.shouldSendNextRequest(timeoutMSec, currTime)) {
215 conn.queueAndSend(new SendDataPck(addr, true, command));
216 this.requestCurrentPckCommandWithAck.onRequestSent(currTime);
218 } catch (LcnException e) {
219 logger.warn("{}: Could not send command: {}: {}", addr, new String(command, LcnDefs.LCN_ENCODING),
226 * Triggers a request to retrieve the firmware version of the LCN module, if it is not known, yet.
228 public void requestFirmwareVersion() {
229 if (firmwareVersion == -1) {
230 requestFirmwareVersion.refresh();
235 * Used to check if the module has the measurement processing firmware (since Feb. 2013).
237 * @return if the module has at least 4 threshold registers and 12 variables
239 public boolean hasExtendedMeasurementProcessing() {
240 if (firmwareVersion == -1) {
241 logger.warn("LCN module firmware version unknown");
244 return firmwareVersion >= LcnBindingConstants.FIRMWARE_2013;
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);
258 * Keeps the request logic active.
259 * Must be called periodically.
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
265 void update(Connection conn, long timeoutMSec, long currTime) {
267 if (update(conn, timeoutMSec, currTime, requestFirmwareVersion, PckGenerator.requestSn())) {
271 for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
272 if (update(conn, timeoutMSec, currTime, requestStatusOutputs[i], PckGenerator.requestOutputStatus(i))) {
277 if (update(conn, timeoutMSec, currTime, requestStatusRelays, PckGenerator.requestRelaysStatus())) {
280 if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) {
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;
294 for (Map.Entry<Variable, RequestStatus> kv : this.requestStatusVars.entrySet()) {
295 RequestStatus requestStatus = kv.getValue();
296 if (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) {
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();
308 } catch (LcnException ex) {
309 requestStatus.reset();
316 if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps,
317 PckGenerator.requestLedsAndLogicOpsStatus())) {
321 if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) {
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());
333 * Gets the LCN module's firmware date.
337 public int getFirmwareVersion() {
338 return this.firmwareVersion;
342 * Sets the LCN module's firmware date.
344 * @param firmwareVersion the date
346 public void setFirmwareVersion(int firmwareVersion) {
347 this.firmwareVersion = firmwareVersion;
349 requestFirmwareVersion.onResponseReceived();
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 value.setMaxAgeMSec(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC);
359 * Updates the variable value cache.
361 * @param variable the variable to update
362 * @param value the new value
364 public void updateVariableValue(Variable variable, VariableValue value) {
365 variableValue.put(variable, value);
369 * Gets the current value of a variable from the cache.
371 * @param variable the variable to retrieve the value for
372 * @return the value of the variable
373 * @throws LcnException when the variable is not in the cache
375 public long getVariableValue(Variable variable) throws LcnException {
376 return Optional.ofNullable(variableValue.get(variable)).map(v -> v.toNative(variable.useLcnSpecialValues()))
377 .orElseThrow(() -> new LcnException("Current variable value unknown"));
381 * Requests the current value of all dimmer outputs.
383 public void refreshAllOutputs() {
384 Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh);
388 * Requests the current value of the given dimmer output.
392 public void refreshOutput(int number) {
393 requestStatusOutputs[number].refresh();
397 * Requests the current value of all relays.
399 public void refreshRelays() {
400 requestStatusRelays.refresh();
404 * Requests the current value of all binary sensor.
406 public void refreshBinarySensors() {
407 requestStatusBinSensors.refresh();
411 * Requests the current value of the given variable.
413 * @param variable the variable to request
415 public void refreshVariable(Variable variable) {
416 RequestStatus requestStatus = requestStatusVars.get(variable);
417 if (requestStatus != null) {
418 requestStatus.refresh();
423 * Requests the current value of all LEDs and logic operations.
425 public void refreshLedsAndLogic() {
426 requestStatusLedsAndLogicOps.refresh();
430 * Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB.
432 public void refreshStatusLedsAnLogicAfterChange() {
433 requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
437 * Requests the current locking states of all keys.
439 public void refreshStatusLockedKeys() {
440 requestStatusLockedKeys.refresh();
444 * Requests the current locking states of all keys, after a lock state has been changed by openHAB.
446 public void refreshStatusStatusLockedKeysAfterChange() {
447 requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
451 * Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output
453 * @param outputId 0..3
455 public void onOutputResponseReceived(int outputId) {
456 requestStatusOutputs[outputId].onResponseReceived();
460 * Resets the value request logic, when a requested value has been received from the LCN module: Relay
462 public void onRelayResponseReceived() {
463 requestStatusRelays.onResponseReceived();
467 * Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor
469 public void onBinarySensorsResponseReceived() {
470 requestStatusBinSensors.onResponseReceived();
474 * Resets the value request logic, when a requested value has been received from the LCN module: Variable
476 * @param variable the received variable type
478 public void onVariableResponseReceived(Variable variable) {
479 RequestStatus requestStatus = requestStatusVars.get(variable);
480 if (requestStatus != null) {
481 requestStatus.onResponseReceived();
486 * Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic
488 public void onLedsAndLogicResponseReceived() {
489 requestStatusLedsAndLogicOps.onResponseReceived();
493 * Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state
495 public void onLockedKeysResponseReceived() {
496 requestStatusLockedKeys.onResponseReceived();