/**
* 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.common;
/**
* Helpers to generate LCN-PCK commands.
* <p>
* LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands.
*
* @author Tobias J�ttner
*/
public final class PckGenerator {
/** Terminates a PCK command. */
public static final String TERMINATION = "\n";
/**
* Generates a keep-alive.
* LCN-PCHK will close the connection if it does not receive any commands from
* an open {@link Connection} for a specific period (10 minutes by default).
*
* @param counter the current ping's id (optional, but "best practice"). Should start with 1
* @return the PCK command as text
*/
public static String ping(int counter) {
return String.format("^ping%d", counter);
}
/**
* Generates a PCK command that will set the LCN-PCHK connection's operation mode.
* This influences how output-port commands and status are interpreted and must be
* in sync with the LCN bus.
*
* @param dimMode see {@link LcnDefs.OutputPortDimMode}
* @param statusMode see {@link LcnDefs.OutputPortStatusMode}
* @return the PCK command as text
*/
public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) {
return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.STEPS200 ? "1" : "0")
+ (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N");
}
/**
* Generates a PCK address header.
* Used for commands to LCN modules and groups.
*
* @param addr the target's address (module or group)
* @param localSegId the local segment id where the physical bus connection is located
* @param wantsAck true to claim an acknowledge / receipt from the target
* @return the PCK address header as text
*/
public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) {
return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegId(localSegId),
addr.getId(), wantsAck ? "!" : ".");
}
/**
* Generates a scan-command for LCN segment-couplers.
* Used to detect the local segment (where the physical bus connection is located).
*
* @return the PCK command (without address header) as text
*/
public static String segmentCouplerScan() {
return "SK";
}
/**
* Generates a firmware/serial-number request.
*
* @return the PCK command (without address header) as text
*/
public static String requestSn() {
return "SN";
}
/**
* Generates an output-port status request.
*
* @param outputId 0..3
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String requestOutputStatus(int outputId) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
return String.format("SMA%d", outputId + 1);
}
/**
* Generates a dim command for a single output-port.
*
* @param outputId 0..3
* @param percent 0..100
* @param ramp use {@link LcnDefs#timeToRampValue(int)}
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String dimOutput(int outputId, double percent, int ramp) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
int n = (int) Math.round(percent * 2);
if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
return String.format("A%dDI%03d%03d", outputId + 1, n / 2, ramp);
} else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
return String.format("O%dDI%03d%03d", outputId + 1, n, ramp);
}
}
/**
* Generates a dim command for all output-ports.
*
* @param percent 0..100
* @param ramp use {@link LcnDefs#timeToRampValue(int)} (might be ignored in some cases)
* @param is1805 true if the target module's firmware is 180501 or newer
* @return the PCK command (without address header) as text
*/
public static String dimAllOutputs(double percent, int ramp, boolean is1805) {
int n = (int) Math.round(percent * 2);
if (is1805) {
return String.format("OY%03d%03d%03d%03d%03d", n, n, n, n, ramp); // Supported since LCN-PCHK 2.61
}
if (n == 0) { // All off
return String.format("AA%03d", ramp);
} else if (n == 200) { // All on
return String.format("AE%03d", ramp);
}
// This is our worst-case: No high-res, no ramp
return String.format("AH%03d", n / 2);
}
/**
* Generates a command to change the value of an output-port.
*
* @param outputId 0..3
* @param percent -100..100
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String relOutput(int outputId, double percent) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
int n = (int) Math.round(percent * 2);
if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2));
} else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n));
}
}
/**
* Generates a command that toggles a single output-port (on->off, off->on).
*
* @param outputId 0..3
* @param ramp see {@link LcnDefs#timeToRampValue(int)}
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String toggleOutput(int outputId, int ramp) throws IllegalArgumentException {
if (outputId < 0 || outputId > 3) {
throw new IllegalArgumentException();
}
return String.format("A%dTA%03d", outputId + 1, ramp);
}
/**
* Generates a command that toggles all output-ports (on->off, off->on).
*
* @param ramp see {@link LcnDefs#timeToRampValue(int)}
* @return the PCK command (without address header) as text
*/
public static String toggleAllOutputs(int ramp) {
return String.format("AU%03d", ramp);
}
/**
* Generates a relays-status request.
*
* @return the PCK command (without address header) as text
*/
public static String requestRelaysStatus() {
return "SMR";
}
/**
* Generates a command to control relays.
*
* @param states the 8 modifiers for the relay states
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws IllegalArgumentException {
if (states.length != 8) {
throw new IllegalArgumentException();
}
String ret = "R8";
for (int i = 0; i < 8; ++i) {
switch (states[i]) {
case ON:
ret += "1";
break;
case OFF:
ret += "0";
break;
case TOGGLE:
ret += "U";
break;
case NOCHANGE:
ret += "-";
break;
default:
throw new Error();
}
}
return ret;
}
/**
* Generates a binary-sensors status request.
*
* @return the PCK command (without address header) as text
*/
public static String requestBinSensorsStatus() {
return "SMB";
}
/**
* Generates a command that sets a variable absolute.
*
* @param var the target variable to set
* @param value the absolute value to set
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException
*/
public static String varAbs(LcnDefs.Var var, int value) throws IllegalArgumentException {
int id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
// Set absolute (not in PCK yet)
int b1 = id << 6; // 01000000
b1 |= 0x20; // xx10xxxx (set absolute)
value -= 1000; // Offset
b1 |= (value >> 8) & 0x0f; // xxxx1111
int b2 = value & 0xff;
return String.format("X2%03d%03d%03d", 30, b1, b2);
}
// Setting variables and thresholds absolute not implemented in LCN firmware yet
throw new IllegalArgumentException();
}
/**
* Generates a command that send variable status updates.
* PCHK provides this variables by itself on selected segments
* is only possible with group 4
*
* @param var the target variable to set
* @param value the absolute value to set
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException
*/
public static String updateStatusVar(LcnDefs.Var var, int value) throws IllegalArgumentException {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
// define variable to set, offset 0x01000000
int x2cmd = id | 0x40;
int b1 = (value >> 8) & 0xff;
int b2 = value & 0xff;
return String.format("X2%03d%03d%03d", x2cmd, b1, b2);
}
// Setting variables and thresholds absolute not implemented in LCN firmware yet
throw new IllegalArgumentException();
}
/**
* Generates a command that resets a variable to 0.
*
* @param var the target variable to set 0
* @param is2013 the target module's firmware version is 170101 or newer
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if command is not supported
*/
public static String varReset(LcnDefs.Var var, boolean is2013) throws IllegalArgumentException {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
if (is2013) {
return String.format("Z-%03d%04d", id + 1, 4090);
} else {
if (id == 0) {
return "ZS30000";
} else {
throw new IllegalArgumentException();
}
}
}
id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
// Set absolute = 0 (not in PCK yet)
int b1 = id << 6; // 01000000
b1 |= 0x20; // xx10xxxx (set absolute)
int b2 = 0;
return String.format("X2%03d%03d%03d", 30, b1, b2);
}
// Reset for threshold not implemented in LCN firmware yet
throw new IllegalArgumentException();
}
/**
* Generates a command to change the value of a variable.
*
* @param var the target variable to change
* @param type the reference-point
* @param value the native LCN value to add/subtract (can be negative)
* @param is2013 the target module's firmware version is 170101 or newer
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if command is not supported
*/
public static String varRel(LcnDefs.Var var, LcnDefs.RelVarRef type, int value, boolean is2013)
throws IllegalArgumentException {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
if (id == 0) { // Old command for variable 1 / T-var (compatible with all modules)
return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value));
} else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8)
return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", id + 1, Math.abs(value));
}
}
id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
return String.format("RE%sS%s%s%d", id == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P",
value >= 0 ? "+" : "-", Math.abs(value));
}
int registerId = LcnDefs.Var.toThrsRegisterId(var);
id = LcnDefs.Var.toThrsId(var);
if (registerId != -1 && id != -1) {
if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8)
return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
value >= 0 ? "A" : "S", registerId + 1, id + 1);
} else if (registerId == 0) { // Old command for register 1 (before 170206)
return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E",
Math.abs(value), value >= 0 ? "A" : "S", id == 0 ? "1" : "0", id == 1 ? "1" : "0",
id == 2 ? "1" : "0", id == 3 ? "1" : "0", id == 4 ? "1" : "0");
}
}
throw new IllegalArgumentException();
}
/**
* Generates a variable value request.
*
* @param var the variable to request
* @param swAge the target module's firmware version
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if command is not supported
*/
public static String requestVarStatus(LcnDefs.Var var, int swAge) throws IllegalArgumentException {
if (swAge >= 0x170206) {
int id = LcnDefs.Var.toVarId(var);
if (id != -1) {
return String.format("MWT%03d", id + 1);
}
id = LcnDefs.Var.toSetPointId(var);
if (id != -1) {
return String.format("MWS%03d", id + 1);
}
id = LcnDefs.Var.toThrsRegisterId(var);
if (id != -1) {
return String.format("SE%03d", id + 1); // Whole register
}
id = LcnDefs.Var.toS0Id(var);
if (id != -1) {
return String.format("MWC%03d", id + 1);
}
throw new IllegalArgumentException();
} else {
switch (var) {
case VAR1ORTVAR:
return "MWV";
case VAR2ORR1VAR:
return "MWTA";
case VAR3ORR2VAR:
return "MWTB";
case R1VARSETPOINT:
return "MWSA";
case R2VARSETPOINT:
return "MWSB";
case THRS1:
case THRS2:
case THRS3:
case THRS4:
case THRS5:
return "SL1"; // Whole register
default:
throw new IllegalArgumentException();
}
}
}
/**
* Generates a request for LED and logic-operations states.
*
* @return the PCK command (without address header) as text
*/
public static String requestLedsAndLogicOpsStatus() {
return "SMT";
}
/**
* Generates a command to the set the state of a single LED.
*
* @param ledId 0..11
* @param state the state to set
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String controlLed(int ledId, LcnDefs.LedStatus state) throws IllegalArgumentException {
if (ledId < 0 || ledId > 11) {
throw new IllegalArgumentException();
}
return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A"
: state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F");
}
/**
* Generates a command to send LCN keys.
*
* @param cmds the 4 concrete commands to send for the tables (A-D)
* @param keys the tables' 8 key-states (true means "send")
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws IllegalArgumentException {
if (cmds.length != 4 || keys.length != 8) {
throw new IllegalArgumentException();
}
String ret = "TS";
for (int i = 0; i < 4; ++i) {
switch (cmds[i]) {
case HIT:
ret += "K";
break;
case MAKE:
ret += "L";
break;
case BREAK:
ret += "O";
break;
case DONTSEND:
// By skipping table D (if it is not used), we use the old command
// for table A-C which is compatible with older LCN modules
if (i < 3) {
ret += "-";
}
break;
default:
throw new Error();
}
}
for (int i = 0; i < 8; ++i) {
ret += keys[i] ? "1" : "0";
}
return ret;
}
/**
* Generates a command to send LCN keys deferred / delayed.
*
* @param tableId 0(A)..3(D)
* @param time the delay time
* @param timeUnit the time unit
* @param keys the key-states (true means "send")
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
throws IllegalArgumentException {
if (tableId < 0 || tableId > 3 || keys.length != 8) {
throw new IllegalArgumentException();
}
String ret = "TV";
switch (tableId) {
case 0:
ret += "A";
break;
case 1:
ret += "B";
break;
case 2:
ret += "C";
break;
case 3:
ret += "D";
break;
default:
throw new IllegalArgumentException();
}
ret += String.format("%03d", time);
switch (timeUnit) {
case SECONDS:
if (time < 1 || time > 60) {
throw new IllegalArgumentException();
}
ret += "S";
break;
case MINUTES:
if (time < 1 || time > 90) {
throw new IllegalArgumentException();
}
ret += "M";
break;
case HOURS:
if (time < 1 || time > 50) {
throw new IllegalArgumentException();
}
ret += "H";
break;
case DAYS:
if (time < 1 || time > 45) {
throw new IllegalArgumentException();
}
ret += "D";
break;
default:
throw new Error();
}
for (int i = 0; i < 8; ++i) {
ret += keys[i] ? "1" : "0";
}
return ret;
}
/**
* Generates a request for key-lock states.
* Always requests table A-D. Supported since LCN-PCHK 2.8.
*
* @return the PCK command (without address header) as text
*/
public static String requestKeyLocksStatus() {
return "STX";
}
/**
* Generates a command to lock keys.
*
* @param tableId 0(A)..3(D)
* @param states the 8 key-lock modifiers
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws IllegalArgumentException {
if (tableId < 0 || tableId > 3 || states.length != 8) {
throw new IllegalArgumentException();
}
String ret = String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D");
for (int i = 0; i < 8; ++i) {
switch (states[i]) {
case ON:
ret += "1";
break;
case OFF:
ret += "0";
break;
case TOGGLE:
ret += "U";
break;
case NOCHANGE:
ret += "-";
break;
default:
throw new Error();
}
}
return ret;
}
/**
* Generates a command to lock keys for table A temporary.
* There is no hardware-support for locking tables B-D.
*
* @param time the lock time
* @param timeUnit the time unit
* @param keys the 8 key-lock states (true means lock)
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
throws IllegalArgumentException {
if (keys.length != 8) {
throw new IllegalArgumentException();
}
String ret = String.format("TXZA%03d", time);
switch (timeUnit) {
case SECONDS:
if (time < 1 || time > 60) {
throw new IllegalArgumentException();
}
ret += "S";
break;
case MINUTES:
if (time < 1 || time > 90) {
throw new IllegalArgumentException();
}
ret += "M";
break;
case HOURS:
if (time < 1 || time > 50) {
throw new IllegalArgumentException();
}
ret += "H";
break;
case DAYS:
if (time < 1 || time > 45) {
throw new IllegalArgumentException();
}
ret += "D";
break;
default:
throw new Error();
}
for (int i = 0; i < 8; ++i) {
ret += keys[i] ? "1" : "0";
}
return ret;
}
/**
* Generates the command header / start for sending dynamic texts.
* Used by LCN-GTxD periphery (supports 4 text rows).
* To complete the command, the text to send must be appended (UTF-8 encoding).
* Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each.
*
* @param row 0..3
* @param part 0..4
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String dynTextHeader(int row, int part) throws IllegalArgumentException {
if (row < 0 || row > 3 || part < 0 || part > 4) {
throw new IllegalArgumentException();
}
return String.format("GTDT%d%d", row + 1, part + 1);
}
/**
* Generates a command to lock a regulator.
*
* @param regId 0..1
* @param state the lock state
* @return the PCK command (without address header) as text
* @throws IllegalArgumentException if out of range
*/
public static String lockRegulator(int regId, boolean state) throws IllegalArgumentException {
if (regId < 0 || regId > 1) {
throw new IllegalArgumentException();
}
return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A");
}
}