[rotel] Set model and firmware properties (#13240)

For all models providing these information and relying on ASCII protocol.
The binding now supports reading of status message containing variable length content.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2022-08-12 16:51:54 +02:00 committed by GitHub
parent 9b128c2f7d
commit 2f808bc8dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 38 deletions

View File

@ -263,9 +263,30 @@ public class RotelBindingConstants {
public static final String KEY_TONE_MAX = "tone_max";
public static final String KEY1_PLAY_STATUS = "play_status";
public static final String KEY2_PLAY_STATUS = "status";
public static final String KEY_DISC_NAME = "disc_name";
public static final String KEY_DISC_TYPE = "disc_type";
public static final String KEY_TRACK = "track";
public static final String KEY_TRACK_NAME = "track_name";
public static final String KEY_TIME = "time";
public static final String KEY_RANDOM = "rnd";
public static final String KEY_REPEAT = "rpt";
public static final String KEY_PRESET_FM = "preset_fm";
public static final String KEY_FM_PRESET = "fm_preset_";
public static final String KEY_FM_ALL_PRESET = "fm_allpreset_";
public static final String KEY_FM = "fm";
public static final String KEY_FM_MONO = "fm_mono";
public static final String KEY_FM_RDS = "fm_rds";
public static final String KEY_FM_FREQ = "fm_freq";
public static final String KEY_PRESET_DAB = "preset_dab";
public static final String KEY_DAB_PRESET = "dab_preset_";
public static final String KEY_DAB_ALL_PRESET = "dab_allpreset_";
public static final String KEY_DAB = "dab";
public static final String KEY_DAB_STATION = "dab_station";
public static final String KEY_PRESET_IRADIO = "preset_iradio";
public static final String KEY_IRADIO_PRESET = "iradio_preset_";
public static final String KEY_IRADIO_ALL_PRESET = "iradio_allpreset_";
public static final String KEY_CURRENT_STATION = "current_station";
public static final String KEY_SIGNAL_STRENGTH = "signal_strength";
public static final String KEY_DIMMER = "dimmer";
public static final String KEY_FREQ = "freq";
public static final String KEY_FREQ_ZONE1 = "freq_zone1";
@ -291,8 +312,16 @@ public class RotelBindingConstants {
public static final String KEY_CEILING_REAR_RIGHT_LEVEL = "ceiling_rear_right";
public static final String KEY_CEILING_REAR_LEFT_LEVEL = "ceiling_rear_left";
public static final String KEY_PCUSB_CLASS = "pcusb_class";
public static final String KEY_PRODUCT_TYPE = "product_type";
public static final String KEY_MODEL = "model";
public static final String KEY_PRODUCT_VERSION = "product_version";
public static final String KEY_VERSION = "version";
public static final String KEY_TC_VERSION = "tc_version";
public static final String KEY_DISPLAY = "display";
public static final String KEY_DISPLAY1 = "display1";
public static final String KEY_DISPLAY2 = "display2";
public static final String KEY_DISPLAY3 = "display3";
public static final String KEY_DISPLAY4 = "display4";
// Output keys only used by the HEX protocol
public static final String KEY_LINE1 = "line1";
public static final String KEY_LINE2 = "line2";

View File

@ -496,8 +496,8 @@ public enum RotelCommand {
HDMI_TV_MODE("HDMI TV Mode", PRIMARY_CMD, (byte) 0x79),
ROOM_EQ_TOGGLE("Temporary Room EQ Toggle", PRIMARY_CMD, (byte) 0x67),
SPEAKER_SETTING_TOGGLE("Speaker Level Setting Toggle", PRIMARY_CMD, (byte) 0xA1),
MODEL("Request the model number", null, "model?"),
VERSION("Request the main CPU software version", null, "version?");
MODEL("Request the model number", "get_product_type", "model?"),
VERSION("Request the main CPU software version", "get_product_version", "version?");
public static final List<RotelCommand> DSP_CMDS_SET1 = List.of(DSP_TOGGLE, PROLOGIC_TOGGLE, DOLBY_TOGGLE,
PLII_PANORAMA_TOGGLE, PLII_DIMENSION_UP, PLII_DIMENSION_DOWN, PLII_CENTER_WIDTH_UP, PLII_CENTER_WIDTH_DOWN,

View File

@ -45,6 +45,7 @@ public class RotelSimuConnector extends RotelConnector {
private static final int STEP_TONE_LEVEL = 1;
private static final double STEP_DECIBEL = 0.5;
private static final String FIRMWARE = "V1.1.8";
private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
@ -175,6 +176,7 @@ public class RotelSimuConnector extends RotelConnector {
String textLine1Right = buildVolumeLine1RightResponse();
String textLine2 = "";
String textAscii = "";
boolean variableLength = false;
boolean accepted = true;
boolean resetZone = true;
int numZone = 0;
@ -1062,10 +1064,22 @@ public class RotelSimuConnector extends RotelConnector {
textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
break;
case MODEL:
textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
if (protocol == RotelProtocol.ASCII_V1) {
variableLength = true;
textAscii = buildAsciiResponse(KEY_PRODUCT_TYPE,
String.format("%d,%s", model.getName().length(), model.getName()));
} else {
textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
}
break;
case VERSION:
textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
if (protocol == RotelProtocol.ASCII_V1) {
variableLength = true;
textAscii = buildAsciiResponse(KEY_PRODUCT_VERSION,
String.format("%d,%s", FIRMWARE.length(), FIRMWARE));
} else {
textAscii = buildAsciiResponse(KEY_VERSION, FIRMWARE);
}
break;
default:
accepted = false;
@ -1186,7 +1200,14 @@ public class RotelSimuConnector extends RotelConnector {
idxInFeedbackMsg = 0;
}
} else {
String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
String command = textAscii;
if (protocol == RotelProtocol.ASCII_V1 && !variableLength) {
command += "!";
} else if (protocol == RotelProtocol.ASCII_V2 && !variableLength) {
command += "$";
} else if (protocol == RotelProtocol.ASCII_V2 && variableLength) {
command += "$$";
}
synchronized (lock) {
feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
idxInFeedbackMsg = 0;

View File

@ -1696,9 +1696,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
case KEY_PCUSB_CLASS:
logger.debug("PC-USB Audio Class is set to {}", value);
break;
case KEY_PRODUCT_TYPE:
case KEY_MODEL:
getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
break;
case KEY_PRODUCT_VERSION:
case KEY_VERSION:
getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
break;
@ -1944,6 +1946,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
sendCommand(RotelCommand.SPEAKER);
Thread.sleep(SLEEP_INTV);
}
if (model != RotelModel.RAP1580 && model != RotelModel.RSP1576
&& model != RotelModel.RSP1582) {
sendCommand(RotelCommand.MODEL);
Thread.sleep(SLEEP_INTV);
sendCommand(RotelCommand.VERSION);
Thread.sleep(SLEEP_INTV);
}
break;
case ASCII_V2:
sendCommand(RotelCommand.UPDATE_AUTO);

View File

@ -56,10 +56,10 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro
/** Empty table of special characters */
public static final byte[][] NO_SPECIAL_CHARACTERS = {};
private static final int MAX_SIZE_RESPONSE = 128;
private final Logger logger = LoggerFactory.getLogger(RotelAbstractAsciiProtocolHandler.class);
private final char terminatingChar;
private final int size;
private final byte[] dataBuffer;
private int index;
@ -68,31 +68,31 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro
* Constructor
*
* @param model the Rotel model in use
* @param protocol the protocol to be used
*/
public RotelAbstractAsciiProtocolHandler(RotelModel model, char terminatingChar) {
public RotelAbstractAsciiProtocolHandler(RotelModel model) {
super(model);
this.terminatingChar = terminatingChar;
this.size = 64;
this.dataBuffer = new byte[size];
this.dataBuffer = new byte[MAX_SIZE_RESPONSE];
this.index = 0;
}
@Override
public void handleIncomingData(byte[] inDataBuffer, int length) {
for (int i = 0; i < length; i++) {
if (index < size) {
dataBuffer[index++] = inDataBuffer[i];
}
if (inDataBuffer[i] == terminatingChar) {
if (index >= size) {
dataBuffer[index - 1] = (byte) terminatingChar;
}
byte[] msg = Arrays.copyOf(dataBuffer, index);
handleIncomingMessage(msg);
index = 0;
}
protected boolean fillDataBuffer(byte data) {
if (index < MAX_SIZE_RESPONSE) {
dataBuffer[index++] = data;
return true;
}
return false;
}
protected byte[] getDataBuffer() {
return Arrays.copyOf(dataBuffer, index);
}
protected void resetDataBuffer() {
index = 0;
}
protected int getRemainingSizeInDataBuffer() {
return MAX_SIZE_RESPONSE - index;
}
/**
@ -109,12 +109,6 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro
logger.debug("Unexpected message length: {}", responseMessage.length);
throw new RotelException("Unexpected message length");
}
if (responseMessage[responseMessage.length - 1] != '!' && responseMessage[responseMessage.length - 1] != '$') {
logger.debug("Unexpected ending character in response: {}",
Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF));
throw new RotelException("Unexpected ending character in response");
}
}
/**
@ -133,18 +127,19 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro
}
}
String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII);
String value = new String(message, 0, message.length, StandardCharsets.US_ASCII);
logger.debug("handleValidAsciiMessage: chars *{}*", value);
value = value.trim();
if (value.isEmpty()) {
return;
}
try {
String[] splittedValue = value.split("=");
if (splittedValue.length != 2) {
int idxSeparator = value.indexOf("=");
if (idxSeparator < 0) {
logger.debug("handleValidAsciiMessage: ignored message {}", value);
} else {
dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
dispatchKeyValue(value.substring(0, idxSeparator).trim().toLowerCase(),
value.substring(idxSeparator + 1));
}
} catch (PatternSyntaxException e) {
logger.debug("handleValidAsciiMessage: ignored message {}", value);

View File

@ -12,7 +12,11 @@
*/
package org.openhab.binding.rotel.internal.protocol.ascii;
import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -33,15 +37,26 @@ public class RotelAsciiV1ProtocolHandler extends RotelAbstractAsciiProtocolHandl
private static final char CHAR_END_RESPONSE = '!';
private static final Set<String> KEYSET1 = Set.of(KEY_DISPLAY, KEY_DISPLAY1, KEY_DISPLAY2, KEY_DISPLAY3,
KEY_DISPLAY4, KEY_PRODUCT_TYPE, KEY_PRODUCT_VERSION, KEY_TC_VERSION, KEY_TRACK);
private static final Set<String> KEYSET2 = Set.of(KEY_FM_PRESET, KEY_FM_ALL_PRESET, KEY_DAB_PRESET,
KEY_DAB_ALL_PRESET, KEY_IRADIO_PRESET, KEY_IRADIO_ALL_PRESET);
private final Logger logger = LoggerFactory.getLogger(RotelAsciiV1ProtocolHandler.class);
private final byte[] lengthBuffer = new byte[8];
private boolean searchKey = true;
private boolean searchLength;
private int valueLength;
private int indexLengthBuffer;
/**
* Constructor
*
* @param model the Rotel model in use
*/
public RotelAsciiV1ProtocolHandler(RotelModel model) {
super(model, CHAR_END_RESPONSE);
super(model);
}
@Override
@ -97,4 +112,61 @@ public class RotelAsciiV1ProtocolHandler extends RotelAbstractAsciiProtocolHandl
logger.debug("Command \"{}\" => {}", cmd, messageStr);
return message;
}
@Override
public void handleIncomingData(byte[] inDataBuffer, int length) {
for (int i = 0; i < length; i++) {
boolean end = false;
if (searchKey && inDataBuffer[i] == '=') {
// End of key reading, check if the value is a fixed or variable length
searchKey = false;
byte[] dataKey = getDataBuffer();
String key = new String(dataKey, 0, dataKey.length, StandardCharsets.US_ASCII).trim();
searchLength = isVariableLengthApplicable(key);
indexLengthBuffer = 0;
valueLength = 0;
logger.trace("handleIncomingData: key = *{}* {}", key, searchLength ? "variable" : "fixed");
fillDataBuffer(inDataBuffer[i]);
} else if (searchKey) {
// Reading key
fillDataBuffer(inDataBuffer[i]);
} else if (searchLength && inDataBuffer[i] == ',') {
// End of value length reading
searchLength = false;
byte[] lengthData = Arrays.copyOf(lengthBuffer, indexLengthBuffer);
String lengthStr = new String(lengthData, 0, lengthData.length, StandardCharsets.US_ASCII);
valueLength = Integer.parseInt(lengthStr);
logger.trace("handleIncomingData: valueLength = {}", valueLength);
if (getRemainingSizeInDataBuffer() < valueLength) {
logger.warn(
"handleIncomingData: the size of the internal buffer is too small, reponse will be truncated");
}
end = valueLength == 0;
} else if (searchLength) {
// Reading value length
lengthBuffer[indexLengthBuffer++] = inDataBuffer[i];
} else if (valueLength > 0) {
// Reading value (variable length)
fillDataBuffer(inDataBuffer[i]);
valueLength--;
end = valueLength == 0;
} else if (inDataBuffer[i] == CHAR_END_RESPONSE) {
// End of value reading
end = true;
} else {
// Reading value (fixed length)
fillDataBuffer(inDataBuffer[i]);
}
if (end) {
handleIncomingMessage(getDataBuffer());
resetDataBuffer();
searchKey = true;
searchLength = false;
}
}
}
private boolean isVariableLengthApplicable(String key) {
return KEYSET1.contains(key) || KEYSET2.stream().filter(k -> key.startsWith(k)).count() > 0;
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.rotel.internal.protocol.ascii;
import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -35,15 +36,22 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl
private static final char CHAR_END_RESPONSE = '$';
private static final Set<String> KEYSET = Set.of(KEY_DISC_NAME, KEY_DISC_TYPE, KEY_TRACK_NAME, KEY_TIME, KEY_FM_RDS,
KEY_DAB_STATION);
private final Logger logger = LoggerFactory.getLogger(RotelAsciiV2ProtocolHandler.class);
private boolean searchKey = true;
private boolean variableLength;
private boolean prevIsEndCharacter;
/**
* Constructor
*
* @param model the Rotel model in use
*/
public RotelAsciiV2ProtocolHandler(RotelModel model) {
super(model, CHAR_END_RESPONSE);
super(model);
}
@Override
@ -116,6 +124,44 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl
return message;
}
@Override
public void handleIncomingData(byte[] inDataBuffer, int length) {
for (int i = 0; i < length; i++) {
boolean end = false;
if (searchKey && inDataBuffer[i] == '=') {
// End of key reading, check if the value is a fixed or variable length
searchKey = false;
byte[] dataKey = getDataBuffer();
String key = new String(dataKey, 0, dataKey.length, StandardCharsets.US_ASCII).trim();
variableLength = KEYSET.contains(key);
logger.trace("handleIncomingData: key = *{}* {}", key, variableLength ? "variable" : "fixed");
fillDataBuffer(inDataBuffer[i]);
} else if (searchKey) {
// Reading key
fillDataBuffer(inDataBuffer[i]);
} else if (inDataBuffer[i] == CHAR_END_RESPONSE) {
end = !variableLength || prevIsEndCharacter;
} else {
if (prevIsEndCharacter) {
// End character inside a variable length value
fillDataBuffer((byte) CHAR_END_RESPONSE);
}
// Reading value
fillDataBuffer(inDataBuffer[i]);
}
if (end) {
// End of value reading
handleIncomingMessage(getDataBuffer());
resetDataBuffer();
searchKey = true;
variableLength = false;
prevIsEndCharacter = false;
} else {
prevIsEndCharacter = inDataBuffer[i] == CHAR_END_RESPONSE;
}
}
}
@Override
protected void dispatchKeyValue(String key, String value) {
// For distribution amplifiers, we need to split certain values to get the value for each zone