/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.lcn.connection; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.LinkedList; import java.util.Map; import java.util.TreeMap; import org.openhab.binding.lcn.common.LcnAddrMod; import org.openhab.binding.lcn.common.LcnDefs; import org.openhab.binding.lcn.common.LcnDefs.Var; import org.openhab.binding.lcn.common.PckGenerator; /** * Holds data of an LCN module. * <ul> * <li>Stores the module's firmware version (if requested) * <li>Manages the scheduling of status-requests * <li>Manages the scheduling of acknowledged commands * </ul> * * @author Tobias J�ttner */ public class ModInfo { /** Total number of request to sent before going into failed-state. */ private static final int NUM_TRIES = 3; /** Poll interval for status values that automatically send their values on change. */ private static final int MAX_STATUS_EVENTBASED_VALUEAGE_MSEC = 600000; /** Poll interval for status values that do not send their values on change (always polled). */ private static final int MAX_STATUS_POLLED_VALUEAGE_MSEC = 30000; /** Status request delay after a command has been send which potentially changed that status. */ public static final int STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC = 2000; /** The LCN module's address. */ private final LcnAddrMod addr; /** Firmware date of the LCN module. -1 means "unknown". */ private int swAge = -1; /** * Gets the LCN module's firmware date. * * @return the date */ public int getSwAge() { return this.swAge; } /** * Sets the LCN module's firmware date. * * @param swAge the date */ public void setSwAge(int swAge) { this.swAge = swAge; } /** Firmware version request status. */ public final RequestStatus requestSwAge = new RequestStatus(-1, NUM_TRIES); /** Output-port request status (0..3). */ public final ArrayList<RequestStatus> requestStatusOutputs = new ArrayList<RequestStatus>(); /** Relays request status (all 8). */ public final RequestStatus requestStatusRelays = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES); /** Binary-sensors request status (all 8). */ public final RequestStatus requestStatusBinSensors = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES); /** * Variables request status. * Lazy initialization: Will be filled once the firmware version is known. */ public final TreeMap<LcnDefs.Var, RequestStatus> requestStatusVars = new TreeMap<LcnDefs.Var, RequestStatus>(); /** LEDs and logic-operations request status (all 12+4). */ public final RequestStatus requestStatusLedsAndLogicOps = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES); /** Key lock-states request status (all tables, A-D). */ public final RequestStatus requestStatusLockedKeys = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES); /** * Holds the last LCN variable requested whose response will not contain the variable's type. * {@link Var#UNKNOWN} means there is currently no such request. */ private LcnDefs.Var lastRequestedVarWithoutTypeInResponse = LcnDefs.Var.UNKNOWN; /** * List of queued PCK commands to be acknowledged by the LCN module. * Commands are always without address header. * Note that the first one might currently be "in progress". */ private final LinkedList<ByteBuffer> pckCommandsWithAck = new LinkedList<ByteBuffer>(); /** Status data for the currently processed {@link PckCommandWithAck}. */ private final RequestStatus requestCurrPckCommandWithAck = new RequestStatus(-1, NUM_TRIES); /** * Constructor. * * @param addr the module's address */ public ModInfo(LcnAddrMod addr) { this.addr = addr; for (int i = 0; i < 4; ++i) { this.requestStatusOutputs.add(new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES)); } } /** * Resets all status requests. * Helpful to re-request initial data in case a new {@link LcnBindingConfig} has been loaded. */ public void resetNotCachedStatusRequests() { for (RequestStatus s : this.requestStatusOutputs) { s.reset(); } this.requestStatusRelays.reset(); this.requestStatusBinSensors.reset(); for (RequestStatus s : this.requestStatusVars.values()) { s.reset(); } this.requestStatusLedsAndLogicOps.reset(); this.requestStatusLockedKeys.reset(); this.lastRequestedVarWithoutTypeInResponse = LcnDefs.Var.UNKNOWN; } /** * Gets the last requested variable whose response will not contain the variables type. * * @return the "typeless" variable */ public LcnDefs.Var getLastRequestedVarWithoutTypeInResponse() { return this.lastRequestedVarWithoutTypeInResponse; } /** * Sets the last requested variable whose response will not contain the variables type. * * @param var the "typeless" variable */ public void setLastRequestedVarWithoutTypeInResponse(LcnDefs.Var var) { this.lastRequestedVarWithoutTypeInResponse = var; } /** * Queues a PCK command to be sent. * It will request an acknowledge from the LCN module on receipt. * If there is no response within the request timeout, the command is retried. * * @param data the PCK command to send (without address header) * @param timeoutMSec the time to wait for a response before retrying a request * @param currTime the current time stamp */ public void queuePckCommandWithAck(ByteBuffer data, Connection conn, long timeoutMSec, long currTime) { this.pckCommandsWithAck.add(data); // Try to process the new acknowledged command. Will do nothing if another one is still in progress. this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime); } /** * Called whenever an acknowledge is received from the LCN module. * * @param code the LCN internal code. -1 means "positive" acknowledge * @param timeoutMSec the time to wait for a response before retrying a request * @param currTime the current time stamp */ public void onAck(int code, Connection conn, long timeoutMSec, long currTime) { if (this.requestCurrPckCommandWithAck.isActive()) { // Check if we wait for an ack. this.pckCommandsWithAck.pollFirst(); this.requestCurrPckCommandWithAck.reset(); // Try to process next acknowledged command this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime); } } /** * Sends the next acknowledged command from the queue. * * @param conn the {@link Connection} belonging to this {@link ModInfo} * @param timeoutMSec the time to wait for a response before retrying a request * @param currTime the current time stamp * @return true if a new command was sent */ private boolean tryProcessNextCommandWithAck(Connection conn, long timeoutMSec, long currTime) { // Use the chance to remove a failed command first if (this.requestCurrPckCommandWithAck.isFailed(timeoutMSec, currTime)) { this.pckCommandsWithAck.pollFirst(); this.requestCurrPckCommandWithAck.reset(); } // Peek new command if (!this.pckCommandsWithAck.isEmpty() && !this.requestCurrPckCommandWithAck.isActive()) { this.requestCurrPckCommandWithAck.nextRequestIn(0, currTime); } ByteBuffer data = this.requestCurrPckCommandWithAck.shouldSendNextRequest(timeoutMSec, currTime) ? this.pckCommandsWithAck.peekFirst() : null; if (data == null) { return false; } conn.queue(new SendData.PckSendData(this.addr, true, data)); this.requestCurrPckCommandWithAck.onRequestSent(currTime); return true; } /** * Keeps the request logic active. * Must be called periodically. * * @param conn the {@link Connection} belonging to this {@link ModInfo} * @param timeoutMSec the time to wait for a response before retrying a request * @param currTime the current time stamp */ void update(Connection conn, long timeoutMSec, long currTime) { // Firmware request RequestStatus r; if ((r = this.requestSwAge).shouldSendNextRequest(timeoutMSec, currTime)) { conn.queue(this.addr, false, PckGenerator.requestSn()); r.onRequestSent(currTime); } // Output-port requests for (int i = 0; i < 4; ++i) { if ((r = this.requestStatusOutputs.get(i)).shouldSendNextRequest(timeoutMSec, currTime)) { conn.queue(this.addr, false, PckGenerator.requestOutputStatus(i)); r.onRequestSent(currTime); } } // Relays request if ((r = this.requestStatusRelays).shouldSendNextRequest(timeoutMSec, currTime)) { conn.queue(this.addr, false, PckGenerator.requestRelaysStatus()); r.onRequestSent(currTime); } // Binary-sensors request if ((r = this.requestStatusBinSensors).shouldSendNextRequest(timeoutMSec, currTime)) { conn.queue(this.addr, false, PckGenerator.requestBinSensorsStatus()); r.onRequestSent(currTime); } // Variable requests if (this.swAge != -1) { // Firmware version is required // Initialize if not done yet (we had to wait for the firmware version) if (this.requestStatusVars.isEmpty()) { for (LcnDefs.Var var : LcnDefs.Var.values()) { if (var != LcnDefs.Var.UNKNOWN) { this.requestStatusVars.put(var, new RequestStatus(swAge >= 0x170206 ? MAX_STATUS_EVENTBASED_VALUEAGE_MSEC : MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES)); } } } // Use the chance to remove a failed "typeless variable" request if (this.lastRequestedVarWithoutTypeInResponse != LcnDefs.Var.UNKNOWN) { if (this.requestStatusVars.get(this.lastRequestedVarWithoutTypeInResponse).isTimeout(timeoutMSec, currTime)) { this.lastRequestedVarWithoutTypeInResponse = LcnDefs.Var.UNKNOWN; } } // Variables for (Map.Entry<LcnDefs.Var, RequestStatus> kv : this.requestStatusVars.entrySet()) { if ((r = kv.getValue()).shouldSendNextRequest(timeoutMSec, currTime)) { // Detect if we can send immediately or if we have to wait for a "typeless" request first boolean hasTypeInResponse = LcnDefs.Var.hasTypeInResponse(kv.getKey(), this.swAge); if (hasTypeInResponse || this.lastRequestedVarWithoutTypeInResponse == LcnDefs.Var.UNKNOWN) { try { conn.queue(this.addr, false, PckGenerator.requestVarStatus(kv.getKey(), this.swAge)); r.onRequestSent(currTime); if (!hasTypeInResponse) { this.lastRequestedVarWithoutTypeInResponse = kv.getKey(); } } catch (IllegalArgumentException ex) { r.reset(); } } } } } // LEDs and logic-operations request if ((r = this.requestStatusLedsAndLogicOps).shouldSendNextRequest(timeoutMSec, currTime)) { conn.queue(this.addr, false, PckGenerator.requestLedsAndLogicOpsStatus()); r.onRequestSent(currTime); } // Key-locks request if ((r = this.requestStatusLockedKeys).shouldSendNextRequest(timeoutMSec, currTime)) { conn.queue(this.addr, false, PckGenerator.requestKeyLocksStatus()); r.onRequestSent(currTime); } // Try to send next acknowledged command. Will also detect failed ones. this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime); } }