2 * Copyright (c) 2010-2023 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. */
65 private Optional<Integer> firmwareVersion = Optional.empty();
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 addr + " " + 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.isEmpty()) {
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.isEmpty()) {
241 logger.warn("LCN module firmware version unknown");
244 return firmwareVersion.map(v -> v >= LcnBindingConstants.FIRMWARE_2013).orElse(false);
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())) {
281 if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) {
285 if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps,
286 PckGenerator.requestLedsAndLogicOpsStatus())) {
290 if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) {
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;
304 for (Map.Entry<Variable, RequestStatus> kv : this.requestStatusVars.entrySet()) {
305 RequestStatus requestStatus = kv.getValue();
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) {
312 conn.queue(this.addr, false,
313 PckGenerator.requestVarStatus(kv.getKey(), firmwareVersion));
314 requestStatus.onRequestSent(currTime);
315 if (!hasTypeInResponse) {
316 this.lastRequestedVarWithoutTypeInResponse = kv.getKey();
319 } catch (LcnException ex) {
320 logger.warn("{}: Failed to generate PCK message: {}: {}", addr, kv.getKey(),
322 requestStatus.reset();
323 lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
327 } catch (LcnException e) {
328 logger.warn("{}: Failed to receive measurement value: {}", addr, e.getMessage());
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());
341 * Gets the LCN module's firmware date.
345 public Optional<Integer> getFirmwareVersion() {
346 return firmwareVersion;
350 * Sets the LCN module's firmware date.
352 * @param firmwareVersion the date
354 public void setFirmwareVersion(int firmwareVersion) {
355 this.firmwareVersion = Optional.of(firmwareVersion);
357 requestFirmwareVersion.onResponseReceived();
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);
367 * Updates the variable value cache.
369 * @param variable the variable to update
370 * @param value the new value
372 public void updateVariableValue(Variable variable, VariableValue value) {
373 variableValue.put(variable, value);
377 * Gets the current value of a variable from the cache.
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
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"));
389 * Requests the current value of all dimmer outputs.
391 public void refreshAllOutputs() {
392 Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh);
396 * Requests the current value of the given dimmer output.
400 public void refreshOutput(int number) {
401 requestStatusOutputs[number].refresh();
405 * Requests the current value of all relays.
407 public void refreshRelays() {
408 requestStatusRelays.refresh();
412 * Requests the current value of all binary sensor.
414 public void refreshBinarySensors() {
415 requestStatusBinSensors.refresh();
419 * Requests the current value of the given variable.
421 * @param variable the variable to request
423 public void refreshVariable(Variable variable) {
424 RequestStatus requestStatus = requestStatusVars.get(variable);
425 if (requestStatus != null) {
426 requestStatus.refresh();
431 * Requests the current value of all LEDs and logic operations.
433 public void refreshLedsAndLogic() {
434 requestStatusLedsAndLogicOps.refresh();
438 * Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB.
440 public void refreshStatusLedsAnLogicAfterChange() {
441 requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.currentTimeMillis());
445 * Requests the current locking states of all keys.
447 public void refreshStatusLockedKeys() {
448 requestStatusLockedKeys.refresh();
452 * Requests the current locking states of all keys, after a lock state has been changed by openHAB.
454 public void refreshStatusStatusLockedKeysAfterChange() {
455 requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.currentTimeMillis());
459 * Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output
461 * @param outputId 0..3
463 public void onOutputResponseReceived(int outputId) {
464 requestStatusOutputs[outputId].onResponseReceived();
468 * Resets the value request logic, when a requested value has been received from the LCN module: Relay
470 public void onRelayResponseReceived() {
471 requestStatusRelays.onResponseReceived();
475 * Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor
477 public void onBinarySensorsResponseReceived() {
478 requestStatusBinSensors.onResponseReceived();
482 * Resets the value request logic, when a requested value has been received from the LCN module: Variable
484 * @param variable the received variable type
486 public void onVariableResponseReceived(Variable variable) {
487 RequestStatus requestStatus = requestStatusVars.get(variable);
488 if (requestStatus != null) {
489 requestStatus.onResponseReceived();
492 if (variable == lastRequestedVarWithoutTypeInResponse) {
493 lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN; // Reset
498 * Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic
500 public void onLedsAndLogicResponseReceived() {
501 requestStatusLedsAndLogicOps.onResponseReceived();
505 * Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state
507 public void onLockedKeysResponseReceived() {
508 requestStatusLockedKeys.onResponseReceived();
512 * Returns the module's bus address.
514 public LcnAddr getAddress() {