]> git.basschouten.com Git - openhab-addons.git/commitdiff
[rotel] Refactor to separate comm handling from protocol handling (#12521)
authorlolodomo <lg.hc@free.fr>
Mon, 18 Apr 2022 17:23:52 +0000 (19:23 +0200)
committerGitHub <noreply@github.com>
Mon, 18 Apr 2022 17:23:52 +0000 (19:23 +0200)
* [rotel] Refactor to separate comm handling from protocol handling

Each protocol is now handled in a separate class.

Allow to reduce the size of class RotelConnector

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
* buildCommandMessage now throwing RotelException

Comment added when RotelException is catched without any specific
handling

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
20 files changed:
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelBindingConstants.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelModel.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelConnector.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelIpConnector.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelMessageEvent.java [deleted file]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelMessageEventListener.java [deleted file]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelProtocol.java [deleted file]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelReaderThread.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSerialConnector.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSimuConnector.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelAbstractProtocolHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelMessageEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelMessageEventListener.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/hex/RotelHexProtocolHandler.java [new file with mode: 0644]

index 8096e62277bc0cd04e04f8c1e7fa7af5459e338b..611228a5cd0ccae3ad64b85da824d1e9760d4fbd 100644 (file)
@@ -161,4 +161,82 @@ public class RotelBindingConstants {
 
     // List of all properties
     public static final String PROPERTY_PROTOCOL = "protocol";
+
+    // Message types (HEX protocol)
+    public static final byte PRIMARY_CMD = (byte) 0x10;
+    public static final byte MAIN_ZONE_CMD = (byte) 0x14;
+    public static final byte RECORD_SRC_CMD = (byte) 0x15;
+    public static final byte ZONE2_CMD = (byte) 0x16;
+    public static final byte ZONE3_CMD = (byte) 0x17;
+    public static final byte ZONE4_CMD = (byte) 0x18;
+    public static final byte VOLUME_CMD = (byte) 0x30;
+    public static final byte ZONE2_VOLUME_CMD = (byte) 0x32;
+    public static final byte ZONE3_VOLUME_CMD = (byte) 0x33;
+    public static final byte ZONE4_VOLUME_CMD = (byte) 0x34;
+    public static final byte TRIGGER_CMD = (byte) 0x40;
+    public static final byte STANDARD_RESPONSE = (byte) 0x20;
+    public static final byte TRIGGER_STATUS = (byte) 0x21;
+    public static final byte SMART_DISPLAY_DATA_1 = (byte) 0x22;
+    public static final byte SMART_DISPLAY_DATA_2 = (byte) 0x23;
+
+    // Common (output) keys used by the HEX and ASCII protocols
+    public static final String KEY_POWER = "power";
+    public static final String KEY_VOLUME = "volume";
+    public static final String KEY_MUTE = "mute";
+    public static final String KEY_BASS = "bass";
+    public static final String KEY_TREBLE = "treble";
+    public static final String KEY_SOURCE = "source";
+    public static final String KEY_DSP_MODE = "dsp_mode";
+    public static final String KEY_ERROR = "error";
+    // Keys only used by the ASCII protocol
+    public static final String KEY_UPDATE_MODE = "update_mode";
+    public static final String KEY_DISPLAY_UPDATE = "display_update";
+    public static final String KEY_VOLUME_MIN = "volume_min";
+    public static final String KEY_VOLUME_MAX = "volume_max";
+    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_TRACK = "track";
+    public static final String KEY_DIMMER = "dimmer";
+    public static final String KEY_FREQ = "freq";
+    public static final String KEY_TONE = "tone";
+    public static final String KEY_TCBYPASS = "bypass";
+    public static final String KEY_BALANCE = "balance";
+    public static final String KEY_SPEAKER = "speaker";
+    // Output keys only used by the HEX protocol
+    public static final String KEY_LINE1 = "line1";
+    public static final String KEY_LINE2 = "line2";
+    public static final String KEY_RECORD = "record";
+    public static final String KEY_RECORD_SEL = "record_sel";
+    public static final String KEY_ZONE = "zone";
+    public static final String KEY_POWER_ZONE2 = "power_zone2";
+    public static final String KEY_POWER_ZONE3 = "power_zone3";
+    public static final String KEY_POWER_ZONE4 = "power_zone4";
+    public static final String KEY_SOURCE_ZONE2 = "source_zone2";
+    public static final String KEY_SOURCE_ZONE3 = "source_zone3";
+    public static final String KEY_SOURCE_ZONE4 = "source_zone4";
+    public static final String KEY_VOLUME_ZONE2 = "volume_zone2";
+    public static final String KEY_VOLUME_ZONE3 = "volume_zone3";
+    public static final String KEY_VOLUME_ZONE4 = "volume_zone4";
+    public static final String KEY_MUTE_ZONE2 = "mute_zone2";
+    public static final String KEY_MUTE_ZONE3 = "mute_zone3";
+    public static final String KEY_MUTE_ZONE4 = "mute_zone4";
+
+    // Specific values for keys
+    public static final String MSG_VALUE_OFF = "off";
+    public static final String MSG_VALUE_ON = "on";
+    public static final String POWER_ON = "on";
+    public static final String STANDBY = "standby";
+    public static final String POWER_OFF_DELAYED = "off_delayed";
+    public static final String MSG_VALUE_SPEAKER_A = "a";
+    public static final String MSG_VALUE_SPEAKER_B = "b";
+    public static final String MSG_VALUE_SPEAKER_AB = "a_b";
+    public static final String MSG_VALUE_MIN = "min";
+    public static final String MSG_VALUE_MAX = "max";
+    public static final String MSG_VALUE_FIX = "fix";
+    public static final String AUTO = "auto";
+    public static final String MANUAL = "manual";
+    public static final String PLAY = "play";
+    public static final String PAUSE = "pause";
+    public static final String STOP = "stop";
 }
index 23c5f794d3442d5816be70c165a2ebff57eb379d..6eb1c5305e8037ee66162ace262f906eb7d0dcc2 100644 (file)
  */
 package org.openhab.binding.rotel.internal;
 
+import static org.openhab.binding.rotel.internal.communication.RotelCommand.*;
+import static org.openhab.binding.rotel.internal.protocol.ascii.RotelAbstractAsciiProtocolHandler.*;
+
 import java.util.ArrayList;
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.rotel.internal.communication.RotelCommand;
-import org.openhab.binding.rotel.internal.communication.RotelConnector;
 import org.openhab.binding.rotel.internal.communication.RotelDsp;
 import org.openhab.binding.rotel.internal.communication.RotelFlagsMapping;
 import org.openhab.binding.rotel.internal.communication.RotelSource;
@@ -32,78 +34,64 @@ import org.openhab.core.types.StateOption;
 @NonNullByDefault
 public enum RotelModel {
 
-    RSP1066("RSP-1066", 19200, 3, 1, false, 90, false, 12, false, RotelCommand.ZONE_SELECT, 1, (byte) 0xC2, 13, 8, true,
+    RSP1066("RSP-1066", 19200, 3, 1, false, 90, false, 12, false, ZONE_SELECT, 1, (byte) 0xC2, 13, 8, true,
+            RotelFlagsMapping.MAPPING1),
+    RSP1068("RSP-1068", 19200, 1, 1, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 2, (byte) 0xA1, 42, 5, true,
+            RotelFlagsMapping.MAPPING2),
+    RSP1069("RSP-1069", 38400, 1, 3, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 2, (byte) 0xA2, 42, 5, true,
+            RotelFlagsMapping.MAPPING5),
+    RSP1098("RSP-1098", 19200, 1, 1, true, 96, true, 6, false, ZONE_SELECT, 2, (byte) 0xA0, 13, 8, true,
             RotelFlagsMapping.MAPPING1),
-    RSP1068("RSP-1068", 19200, 1, 1, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 2, (byte) 0xA1, 42,
-            5, true, RotelFlagsMapping.MAPPING2),
-    RSP1069("RSP-1069", 38400, 1, 3, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 2, (byte) 0xA2, 42,
-            5, true, RotelFlagsMapping.MAPPING5),
-    RSP1098("RSP-1098", 19200, 1, 1, true, 96, true, 6, false, RotelCommand.ZONE_SELECT, 2, (byte) 0xA0, 13, 8, true,
+    RSP1570("RSP-1570", 115200, 1, 3, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 3, (byte) 0xA3, 42, 5, true,
+            RotelFlagsMapping.MAPPING5),
+    RSP1572("RSP-1572", 115200, 2, 3, true, 96, true, null, false, RECORD_FONCTION_SELECT, 4, (byte) 0xA5, 42, 5, true,
+            RotelFlagsMapping.MAPPING5),
+    RSX1055("RSX-1055", 19200, 3, 1, false, 90, false, 12, false, ZONE_SELECT, 1, (byte) 0xC3, 13, 8, true,
             RotelFlagsMapping.MAPPING1),
-    RSP1570("RSP-1570", 115200, 1, 3, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 3, (byte) 0xA3, 42,
-            5, true, RotelFlagsMapping.MAPPING5),
-    RSP1572("RSP-1572", 115200, 2, 3, true, 96, true, null, false, RotelCommand.RECORD_FONCTION_SELECT, 4, (byte) 0xA5,
-            42, 5, true, RotelFlagsMapping.MAPPING5),
-    RSX1055("RSX-1055", 19200, 3, 1, false, 90, false, 12, false, RotelCommand.ZONE_SELECT, 1, (byte) 0xC3, 13, 8, true,
+    RSX1056("RSX-1056", 19200, 1, 1, true, 96, true, 12, false, ZONE_SELECT, 2, (byte) 0xC5, 13, 8, true,
             RotelFlagsMapping.MAPPING1),
-    RSX1056("RSX-1056", 19200, 1, 1, true, 96, true, 12, false, RotelCommand.ZONE_SELECT, 2, (byte) 0xC5, 13, 8, true,
+    RSX1057("RSX-1057", 19200, 1, 1, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 2, (byte) 0xC7, 13, 8, true,
             RotelFlagsMapping.MAPPING1),
-    RSX1057("RSX-1057", 19200, 1, 1, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 2, (byte) 0xC7, 13,
-            8, true, RotelFlagsMapping.MAPPING1),
-    RSX1058("RSX-1058", 38400, 1, 3, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 2, (byte) 0xC8, 13,
-            8, true, RotelFlagsMapping.MAPPING4),
-    RSX1065("RSX-1065", 19200, 3, 1, false, 96, false, 12, false, RotelCommand.ZONE_SELECT, 1, (byte) 0xC1, 42, 5, true,
+    RSX1058("RSX-1058", 38400, 1, 3, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 2, (byte) 0xC8, 13, 8, true,
+            RotelFlagsMapping.MAPPING4),
+    RSX1065("RSX-1065", 19200, 3, 1, false, 96, false, 12, false, ZONE_SELECT, 1, (byte) 0xC1, 42, 5, true,
+            RotelFlagsMapping.MAPPING2),
+    RSX1067("RSX-1067", 19200, 1, 1, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 2, (byte) 0xC4, 42, 5, true,
             RotelFlagsMapping.MAPPING2),
-    RSX1067("RSX-1067", 19200, 1, 1, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 2, (byte) 0xC4, 42,
-            5, true, RotelFlagsMapping.MAPPING2),
-    RSX1550("RSX-1550", 115200, 1, 3, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 3, (byte) 0xC9, 13,
-            8, true, RotelFlagsMapping.MAPPING3),
-    RSX1560("RSX-1560", 115200, 1, 3, true, 96, true, 6, false, RotelCommand.RECORD_FONCTION_SELECT, 3, (byte) 0xCA, 42,
-            5, true, RotelFlagsMapping.MAPPING5),
-    RSX1562("RSX-1562", 115200, 2, 3, true, 96, true, null, false, RotelCommand.RECORD_FONCTION_SELECT, 4, (byte) 0xCC,
-            42, 5, true, RotelFlagsMapping.MAPPING5),
-    A11("A11", 115200, 4, 96, true, 10, 15, false, -1, false, true, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    A12("A12", 115200, 5, 96, true, 10, 15, false, -1, true, true, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    A14("A14", 115200, 5, 96, true, 10, 15, false, -1, true, true, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    CD11("CD11", 57600, 0, null, false, null, true, -1, false, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    CD14("CD14", 57600, 0, null, false, null, true, -1, false, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    RA11("RA-11", 115200, 6, 96, true, 10, 15, true, -1, true, false, false, 6, 0, RotelConnector.SPECIAL_CHARACTERS),
-    RA12("RA-12", 115200, 6, 96, true, 10, 15, true, -1, true, false, false, 6, 0, RotelConnector.SPECIAL_CHARACTERS),
-    RA1570("RA-1570", 115200, 7, 96, true, 10, 15, true, -1, true, true, false, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RA1572("RA-1572", 115200, 8, 96, true, 10, 15, false, -1, true, true, true, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RA1592("RA-1592", 115200, 9, 96, true, 10, 15, false, -1, true, true, true, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RAP1580("RAP-1580", 115200, 11, 96, true, null, false, 5, false, false, -10, 10,
-            RotelConnector.NO_SPECIAL_CHARACTERS),
-    RC1570("RC-1570", 115200, 7, 96, true, 10, 15, true, -1, true, false, false, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RC1572("RC-1572", 115200, 8, 96, true, 10, 15, false, -1, true, false, true, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RC1590("RC-1590", 115200, 9, 96, true, 10, 15, false, -1, true, false, true, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RCD1570("RCD-1570", 115200, 0, null, false, null, true, -1, false, true, 6, 0, RotelConnector.SPECIAL_CHARACTERS),
-    RCD1572("RCD-1572", 57600, 0, null, false, null, true, -1, false, true, 6, 0,
-            RotelConnector.SPECIAL_CHARACTERS_RCD1572),
-    RCX1500("RCX-1500", 115200, 17, 86, true, null, true, -1, false, false, null, null,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RDD1580("RDD-1580", 115200, 15, null, false, null, true, -1, true, false, null, null,
-            RotelConnector.NO_SPECIAL_CHARACTERS),
-    RDG1520("RDG-1520", 115200, 16, null, false, null, true, -1, false, false, null, null,
-            RotelConnector.SPECIAL_CHARACTERS),
-    RSP1576("RSP-1576", 115200, 10, 96, true, null, false, 5, false, false, -10, 10,
-            RotelConnector.NO_SPECIAL_CHARACTERS),
-    RSP1582("RSP-1582", 115200, 11, 96, true, null, false, 6, false, false, -10, 10,
-            RotelConnector.NO_SPECIAL_CHARACTERS),
-    RT11("RT-11", 115200, 12, null, false, null, false, -1, false, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    RT1570("RT-1570", 115200, 14, null, false, null, false, -1, false, true, 6, 0,
-            RotelConnector.NO_SPECIAL_CHARACTERS),
-    T11("T11", 115200, 12, null, false, null, false, -1, false, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    T14("T14", 115200, 13, null, false, null, false, -1, false, true, 6, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    P5("P5", 115200, 20, 96, true, 10, 10, false, -1, true, false, true, 4, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    X3("X3", 115200, 18, 96, true, 10, 10, false, -1, true, false, true, 4, 0, RotelConnector.NO_SPECIAL_CHARACTERS),
-    X5("X5", 115200, 19, 96, true, 10, 10, false, -1, true, false, true, 4, 0, RotelConnector.NO_SPECIAL_CHARACTERS);
+    RSX1550("RSX-1550", 115200, 1, 3, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 3, (byte) 0xC9, 13, 8, true,
+            RotelFlagsMapping.MAPPING3),
+    RSX1560("RSX-1560", 115200, 1, 3, true, 96, true, 6, false, RECORD_FONCTION_SELECT, 3, (byte) 0xCA, 42, 5, true,
+            RotelFlagsMapping.MAPPING5),
+    RSX1562("RSX-1562", 115200, 2, 3, true, 96, true, null, false, RECORD_FONCTION_SELECT, 4, (byte) 0xCC, 42, 5, true,
+            RotelFlagsMapping.MAPPING5),
+    A11("A11", 115200, 4, 96, true, 10, 15, false, -1, false, true, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    A12("A12", 115200, 5, 96, true, 10, 15, false, -1, true, true, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    A14("A14", 115200, 5, 96, true, 10, 15, false, -1, true, true, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    CD11("CD11", 57600, 0, null, false, null, true, -1, false, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    CD14("CD14", 57600, 0, null, false, null, true, -1, false, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    RA11("RA-11", 115200, 6, 96, true, 10, 15, true, -1, true, false, false, 6, 0, SPECIAL_CHARACTERS),
+    RA12("RA-12", 115200, 6, 96, true, 10, 15, true, -1, true, false, false, 6, 0, SPECIAL_CHARACTERS),
+    RA1570("RA-1570", 115200, 7, 96, true, 10, 15, true, -1, true, true, false, 6, 0, SPECIAL_CHARACTERS),
+    RA1572("RA-1572", 115200, 8, 96, true, 10, 15, false, -1, true, true, true, 6, 0, SPECIAL_CHARACTERS),
+    RA1592("RA-1592", 115200, 9, 96, true, 10, 15, false, -1, true, true, true, 6, 0, SPECIAL_CHARACTERS),
+    RAP1580("RAP-1580", 115200, 11, 96, true, null, false, 5, false, false, -10, 10, NO_SPECIAL_CHARACTERS),
+    RC1570("RC-1570", 115200, 7, 96, true, 10, 15, true, -1, true, false, false, 6, 0, SPECIAL_CHARACTERS),
+    RC1572("RC-1572", 115200, 8, 96, true, 10, 15, false, -1, true, false, true, 6, 0, SPECIAL_CHARACTERS),
+    RC1590("RC-1590", 115200, 9, 96, true, 10, 15, false, -1, true, false, true, 6, 0, SPECIAL_CHARACTERS),
+    RCD1570("RCD-1570", 115200, 0, null, false, null, true, -1, false, true, 6, 0, SPECIAL_CHARACTERS),
+    RCD1572("RCD-1572", 57600, 0, null, false, null, true, -1, false, true, 6, 0, SPECIAL_CHARACTERS_RCD1572),
+    RCX1500("RCX-1500", 115200, 17, 86, true, null, true, -1, false, false, null, null, SPECIAL_CHARACTERS),
+    RDD1580("RDD-1580", 115200, 15, null, false, null, true, -1, true, false, null, null, NO_SPECIAL_CHARACTERS),
+    RDG1520("RDG-1520", 115200, 16, null, false, null, true, -1, false, false, null, null, SPECIAL_CHARACTERS),
+    RSP1576("RSP-1576", 115200, 10, 96, true, null, false, 5, false, false, -10, 10, NO_SPECIAL_CHARACTERS),
+    RSP1582("RSP-1582", 115200, 11, 96, true, null, false, 6, false, false, -10, 10, NO_SPECIAL_CHARACTERS),
+    RT11("RT-11", 115200, 12, null, false, null, false, -1, false, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    RT1570("RT-1570", 115200, 14, null, false, null, false, -1, false, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    T11("T11", 115200, 12, null, false, null, false, -1, false, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    T14("T14", 115200, 13, null, false, null, false, -1, false, true, 6, 0, NO_SPECIAL_CHARACTERS),
+    P5("P5", 115200, 20, 96, true, 10, 10, false, -1, true, false, true, 4, 0, NO_SPECIAL_CHARACTERS),
+    X3("X3", 115200, 18, 96, true, 10, 10, false, -1, true, false, true, 4, 0, NO_SPECIAL_CHARACTERS),
+    X5("X5", 115200, 19, 96, true, 10, 10, false, -1, true, false, true, 4, 0, NO_SPECIAL_CHARACTERS);
 
     private String name;
     private int baudRate;
@@ -154,10 +142,9 @@ public enum RotelModel {
             @Nullable Integer volumeMax, boolean directVolume, @Nullable Integer toneLevelMax, boolean playControl,
             @Nullable RotelCommand zoneSelectCmd, int dspCategory, byte deviceId, int respNbChars, int respNbFlags,
             boolean charsBeforeFlags, RotelFlagsMapping flagsMapping) {
-        this(name, baudRate, RotelCommand.DISPLAY_REFRESH, sourceCategory, nbAdditionalZones, additionalCommands,
-                volumeMax, directVolume, toneLevelMax, null, playControl, zoneSelectCmd, dspCategory, false, false,
-                false, null, null, deviceId, respNbChars, respNbFlags, charsBeforeFlags, flagsMapping,
-                RotelConnector.NO_SPECIAL_CHARACTERS);
+        this(name, baudRate, DISPLAY_REFRESH, sourceCategory, nbAdditionalZones, additionalCommands, volumeMax,
+                directVolume, toneLevelMax, null, playControl, zoneSelectCmd, dspCategory, false, false, false, null,
+                null, deviceId, respNbChars, respNbFlags, charsBeforeFlags, flagsMapping, NO_SPECIAL_CHARACTERS);
     }
 
     /**
@@ -181,8 +168,8 @@ public enum RotelModel {
             @Nullable Integer toneLevelMax, boolean playControl, int dspCategory, boolean getFrequencyAvailable,
             boolean getDimmerLevelAvailable, @Nullable Integer diummerLevelMin, @Nullable Integer diummerLevelMax,
             byte[][] specialCharacters) {
-        this(name, baudRate, RotelCommand.POWER, sourceCategory, 0, false, volumeMax, directVolume, toneLevelMax, null,
-                playControl, null, dspCategory, getFrequencyAvailable, false, getDimmerLevelAvailable, diummerLevelMin,
+        this(name, baudRate, POWER, sourceCategory, 0, false, volumeMax, directVolume, toneLevelMax, null, playControl,
+                null, dspCategory, getFrequencyAvailable, false, getDimmerLevelAvailable, diummerLevelMin,
                 diummerLevelMax, (byte) 0, 0, 0, false, RotelFlagsMapping.NO_MAPPING, specialCharacters);
     }
 
@@ -209,8 +196,8 @@ public enum RotelModel {
             @Nullable Integer toneLevelMax, @Nullable Integer balanceLevelMax, boolean playControl, int dspCategory,
             boolean getFrequencyAvailable, boolean getSpeakerGroupsAvailable, boolean getDimmerLevelAvailable,
             @Nullable Integer diummerLevelMin, @Nullable Integer diummerLevelMax, byte[][] specialCharacters) {
-        this(name, baudRate, RotelCommand.POWER, sourceCategory, 0, false, volumeMax, directVolume, toneLevelMax,
-                balanceLevelMax, playControl, null, dspCategory, getFrequencyAvailable, getSpeakerGroupsAvailable,
+        this(name, baudRate, POWER, sourceCategory, 0, false, volumeMax, directVolume, toneLevelMax, balanceLevelMax,
+                playControl, null, dspCategory, getFrequencyAvailable, getSpeakerGroupsAvailable,
                 getDimmerLevelAvailable, diummerLevelMin, diummerLevelMax, (byte) 0, 0, 0, false,
                 RotelFlagsMapping.NO_MAPPING, specialCharacters);
     }
index 2f59fa642bffeb8a8878194c76bca8571cf56095..d2d249737b9648e5e4a98775ff4d730712a11064 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.rotel.internal.communication;
 
+import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.rotel.internal.RotelException;
@@ -24,71 +26,71 @@ import org.openhab.binding.rotel.internal.RotelException;
 @NonNullByDefault
 public enum RotelCommand {
 
-    POWER_TOGGLE("Power Toggle", RotelConnector.PRIMARY_CMD, (byte) 0x0A, "power_toggle", "power_toggle"),
-    POWER_OFF("Power Off", RotelConnector.PRIMARY_CMD, (byte) 0x4A, "power_off", "power_off"),
-    POWER_ON("Power On", RotelConnector.PRIMARY_CMD, (byte) 0x4B, "power_on", "power_on"),
+    POWER_TOGGLE("Power Toggle", PRIMARY_CMD, (byte) 0x0A, "power_toggle", "power_toggle"),
+    POWER_OFF("Power Off", PRIMARY_CMD, (byte) 0x4A, "power_off", "power_off"),
+    POWER_ON("Power On", PRIMARY_CMD, (byte) 0x4B, "power_on", "power_on"),
     POWER("Request current power status", "get_current_power", "power?"),
-    ZONE_SELECT("Zone Select", RotelConnector.PRIMARY_CMD, (byte) 0x23),
-    MAIN_ZONE_POWER_TOGGLE("Main Zone Power Toggle", RotelConnector.MAIN_ZONE_CMD, (byte) 0x0A),
-    MAIN_ZONE_POWER_OFF("Main Zone Power Off", RotelConnector.MAIN_ZONE_CMD, (byte) 0x4A),
-    MAIN_ZONE_POWER_ON("Main Zone Power On", RotelConnector.MAIN_ZONE_CMD, (byte) 0x4B),
-    ZONE2_POWER_TOGGLE("Zone 2 Power Toggle", RotelConnector.ZONE2_CMD, (byte) 0x0A),
-    ZONE2_POWER_OFF("Zone 2 Power Off", RotelConnector.ZONE2_CMD, (byte) 0x4A),
-    ZONE2_POWER_ON("Zone 2 Power On", RotelConnector.ZONE2_CMD, (byte) 0x4B),
-    ZONE3_POWER_TOGGLE("Zone 3 Power Toggle", RotelConnector.ZONE3_CMD, (byte) 0x0A),
-    ZONE3_POWER_OFF("Zone 3 Power Off", RotelConnector.ZONE3_CMD, (byte) 0x4A),
-    ZONE3_POWER_ON("Zone 3 Power On", RotelConnector.ZONE3_CMD, (byte) 0x4B),
-    ZONE4_POWER_TOGGLE("Zone 4 Power Toggle", RotelConnector.ZONE4_CMD, (byte) 0x0A),
-    ZONE4_POWER_OFF("Zone 4 Power Off", RotelConnector.ZONE4_CMD, (byte) 0x4A),
-    ZONE4_POWER_ON("Zone 4 Power On", RotelConnector.ZONE4_CMD, (byte) 0x4B),
-    VOLUME_UP("Volume Up", RotelConnector.PRIMARY_CMD, (byte) 0x0B, "volume_up", "vol_up"),
-    VOLUME_DOWN("Volume Down", RotelConnector.PRIMARY_CMD, (byte) 0x0C, "volume_down", "vol_dwn"),
-    VOLUME_SET("Set Volume to level", RotelConnector.VOLUME_CMD, (byte) 0, "volume_", "vol_"),
+    ZONE_SELECT("Zone Select", PRIMARY_CMD, (byte) 0x23),
+    MAIN_ZONE_POWER_TOGGLE("Main Zone Power Toggle", MAIN_ZONE_CMD, (byte) 0x0A),
+    MAIN_ZONE_POWER_OFF("Main Zone Power Off", MAIN_ZONE_CMD, (byte) 0x4A),
+    MAIN_ZONE_POWER_ON("Main Zone Power On", MAIN_ZONE_CMD, (byte) 0x4B),
+    ZONE2_POWER_TOGGLE("Zone 2 Power Toggle", ZONE2_CMD, (byte) 0x0A),
+    ZONE2_POWER_OFF("Zone 2 Power Off", ZONE2_CMD, (byte) 0x4A),
+    ZONE2_POWER_ON("Zone 2 Power On", ZONE2_CMD, (byte) 0x4B),
+    ZONE3_POWER_TOGGLE("Zone 3 Power Toggle", ZONE3_CMD, (byte) 0x0A),
+    ZONE3_POWER_OFF("Zone 3 Power Off", ZONE3_CMD, (byte) 0x4A),
+    ZONE3_POWER_ON("Zone 3 Power On", ZONE3_CMD, (byte) 0x4B),
+    ZONE4_POWER_TOGGLE("Zone 4 Power Toggle", ZONE4_CMD, (byte) 0x0A),
+    ZONE4_POWER_OFF("Zone 4 Power Off", ZONE4_CMD, (byte) 0x4A),
+    ZONE4_POWER_ON("Zone 4 Power On", ZONE4_CMD, (byte) 0x4B),
+    VOLUME_UP("Volume Up", PRIMARY_CMD, (byte) 0x0B, "volume_up", "vol_up"),
+    VOLUME_DOWN("Volume Down", PRIMARY_CMD, (byte) 0x0C, "volume_down", "vol_dwn"),
+    VOLUME_SET("Set Volume to level", VOLUME_CMD, (byte) 0, "volume_", "vol_"),
     VOLUME_GET("Request current volume level", "get_volume", "volume?"),
     VOLUME_GET_MIN("Request Min volume level", "get_volume_min", null),
     VOLUME_GET_MAX("Request Max volume level", "get_volume_max", null),
-    MUTE_TOGGLE("Mute Toggle", RotelConnector.PRIMARY_CMD, (byte) 0x1E, "mute", "mute"),
+    MUTE_TOGGLE("Mute Toggle", PRIMARY_CMD, (byte) 0x1E, "mute", "mute"),
     MUTE_ON("Mute On", "mute_on", "mute_on"),
     MUTE_OFF("Mute Off", "mute_off", "mute_off"),
     MUTE("Request current mute status", "get_mute_status", "mute?"),
-    MAIN_ZONE_VOLUME_UP("Main Zone Volume Up", RotelConnector.MAIN_ZONE_CMD, (byte) 0),
-    MAIN_ZONE_VOLUME_DOWN("Main Zone Volume Down", RotelConnector.MAIN_ZONE_CMD, (byte) 1),
-    MAIN_ZONE_MUTE_TOGGLE("Main Zone Mute Toggle", RotelConnector.MAIN_ZONE_CMD, (byte) 0x1E),
-    MAIN_ZONE_MUTE_ON("Main Zone Mute On", RotelConnector.MAIN_ZONE_CMD, (byte) 0x6C),
-    MAIN_ZONE_MUTE_OFF("Main Zone Mute Off", RotelConnector.MAIN_ZONE_CMD, (byte) 0x6D),
-    ZONE2_VOLUME_UP("Zone 2 Volume Up", RotelConnector.ZONE2_CMD, (byte) 0),
-    ZONE2_VOLUME_DOWN("Zone 2 Volume Down", RotelConnector.ZONE2_CMD, (byte) 1),
-    ZONE2_VOLUME_SET("Set Zone 2 Volume to level", RotelConnector.ZONE2_VOLUME_CMD, (byte) 0),
-    ZONE2_MUTE_TOGGLE("Zone 2 Mute Toggle", RotelConnector.ZONE2_CMD, (byte) 0x1E),
-    ZONE2_MUTE_ON("Zone 2 Mute On", RotelConnector.ZONE2_CMD, (byte) 0x6C),
-    ZONE2_MUTE_OFF("Zone 2 Mute Off", RotelConnector.ZONE2_CMD, (byte) 0x6D),
-    ZONE3_VOLUME_UP("Zone 3 Volume Up", RotelConnector.ZONE3_CMD, (byte) 0),
-    ZONE3_VOLUME_DOWN("Zone 3 Volume Down", RotelConnector.ZONE3_CMD, (byte) 1),
-    ZONE3_VOLUME_SET("Set Zone 3 Volume to level", RotelConnector.ZONE3_VOLUME_CMD, (byte) 0),
-    ZONE3_MUTE_TOGGLE("Zone 3 Mute Toggle", RotelConnector.ZONE3_CMD, (byte) 0x1E),
-    ZONE3_MUTE_ON("Zone 3 Mute On", RotelConnector.ZONE3_CMD, (byte) 0x6C),
-    ZONE3_MUTE_OFF("Zone 3 Mute Off", RotelConnector.ZONE3_CMD, (byte) 0x6D),
-    ZONE4_VOLUME_UP("Zone 4 Volume Up", RotelConnector.ZONE4_CMD, (byte) 0),
-    ZONE4_VOLUME_DOWN("Zone 4 Volume Down", RotelConnector.ZONE4_CMD, (byte) 1),
-    ZONE4_VOLUME_SET("Set Zone 4 Volume to level", RotelConnector.ZONE4_VOLUME_CMD, (byte) 0),
-    ZONE4_MUTE_TOGGLE("Zone 4 Mute Toggle", RotelConnector.ZONE4_CMD, (byte) 0x1E),
-    ZONE4_MUTE_ON("Zone 4 Mute On", RotelConnector.ZONE4_CMD, (byte) 0x6C),
-    ZONE4_MUTE_OFF("Zone 4 Mute Off", RotelConnector.ZONE4_CMD, (byte) 0x6D),
-    SOURCE_CD("Source CD", RotelConnector.PRIMARY_CMD, (byte) 0x02, "cd", "cd"),
-    SOURCE_TUNER("Source Tuner", RotelConnector.PRIMARY_CMD, (byte) 0x03, "tuner", "tuner"),
-    SOURCE_TAPE("Source Tape", RotelConnector.PRIMARY_CMD, (byte) 0x04, "tape", "tape"),
-    SOURCE_VIDEO1("Source Video 1", RotelConnector.PRIMARY_CMD, (byte) 0x05, "video1", "video1"),
-    SOURCE_VIDEO2("Source Video 2", RotelConnector.PRIMARY_CMD, (byte) 0x06, "video2", "video2"),
-    SOURCE_VIDEO3("Source Video 3", RotelConnector.PRIMARY_CMD, (byte) 0x07, "video3", "video3"),
-    SOURCE_VIDEO4("Source Video 4", RotelConnector.PRIMARY_CMD, (byte) 0x08, "video4", "video4"),
-    SOURCE_VIDEO5("Source Video 5", RotelConnector.PRIMARY_CMD, (byte) 0x09, "video5", "video5"),
-    SOURCE_VIDEO6("Source Video 6", RotelConnector.PRIMARY_CMD, (byte) 0x94, "video6", "video6"),
+    MAIN_ZONE_VOLUME_UP("Main Zone Volume Up", MAIN_ZONE_CMD, (byte) 0),
+    MAIN_ZONE_VOLUME_DOWN("Main Zone Volume Down", MAIN_ZONE_CMD, (byte) 1),
+    MAIN_ZONE_MUTE_TOGGLE("Main Zone Mute Toggle", MAIN_ZONE_CMD, (byte) 0x1E),
+    MAIN_ZONE_MUTE_ON("Main Zone Mute On", MAIN_ZONE_CMD, (byte) 0x6C),
+    MAIN_ZONE_MUTE_OFF("Main Zone Mute Off", MAIN_ZONE_CMD, (byte) 0x6D),
+    ZONE2_VOLUME_UP("Zone 2 Volume Up", ZONE2_CMD, (byte) 0),
+    ZONE2_VOLUME_DOWN("Zone 2 Volume Down", ZONE2_CMD, (byte) 1),
+    ZONE2_VOLUME_SET("Set Zone 2 Volume to level", ZONE2_VOLUME_CMD, (byte) 0),
+    ZONE2_MUTE_TOGGLE("Zone 2 Mute Toggle", ZONE2_CMD, (byte) 0x1E),
+    ZONE2_MUTE_ON("Zone 2 Mute On", ZONE2_CMD, (byte) 0x6C),
+    ZONE2_MUTE_OFF("Zone 2 Mute Off", ZONE2_CMD, (byte) 0x6D),
+    ZONE3_VOLUME_UP("Zone 3 Volume Up", ZONE3_CMD, (byte) 0),
+    ZONE3_VOLUME_DOWN("Zone 3 Volume Down", ZONE3_CMD, (byte) 1),
+    ZONE3_VOLUME_SET("Set Zone 3 Volume to level", ZONE3_VOLUME_CMD, (byte) 0),
+    ZONE3_MUTE_TOGGLE("Zone 3 Mute Toggle", ZONE3_CMD, (byte) 0x1E),
+    ZONE3_MUTE_ON("Zone 3 Mute On", ZONE3_CMD, (byte) 0x6C),
+    ZONE3_MUTE_OFF("Zone 3 Mute Off", ZONE3_CMD, (byte) 0x6D),
+    ZONE4_VOLUME_UP("Zone 4 Volume Up", ZONE4_CMD, (byte) 0),
+    ZONE4_VOLUME_DOWN("Zone 4 Volume Down", ZONE4_CMD, (byte) 1),
+    ZONE4_VOLUME_SET("Set Zone 4 Volume to level", ZONE4_VOLUME_CMD, (byte) 0),
+    ZONE4_MUTE_TOGGLE("Zone 4 Mute Toggle", ZONE4_CMD, (byte) 0x1E),
+    ZONE4_MUTE_ON("Zone 4 Mute On", ZONE4_CMD, (byte) 0x6C),
+    ZONE4_MUTE_OFF("Zone 4 Mute Off", ZONE4_CMD, (byte) 0x6D),
+    SOURCE_CD("Source CD", PRIMARY_CMD, (byte) 0x02, "cd", "cd"),
+    SOURCE_TUNER("Source Tuner", PRIMARY_CMD, (byte) 0x03, "tuner", "tuner"),
+    SOURCE_TAPE("Source Tape", PRIMARY_CMD, (byte) 0x04, "tape", "tape"),
+    SOURCE_VIDEO1("Source Video 1", PRIMARY_CMD, (byte) 0x05, "video1", "video1"),
+    SOURCE_VIDEO2("Source Video 2", PRIMARY_CMD, (byte) 0x06, "video2", "video2"),
+    SOURCE_VIDEO3("Source Video 3", PRIMARY_CMD, (byte) 0x07, "video3", "video3"),
+    SOURCE_VIDEO4("Source Video 4", PRIMARY_CMD, (byte) 0x08, "video4", "video4"),
+    SOURCE_VIDEO5("Source Video 5", PRIMARY_CMD, (byte) 0x09, "video5", "video5"),
+    SOURCE_VIDEO6("Source Video 6", PRIMARY_CMD, (byte) 0x94, "video6", "video6"),
     SOURCE_VIDEO7("Source Video 7", "video7", "video7"),
     SOURCE_VIDEO8("Source Video 8", "video8", "video8"),
-    SOURCE_PHONO("Source Phono", RotelConnector.PRIMARY_CMD, (byte) 0x35, "phono", "phono"),
-    SOURCE_USB("Source Front USB", RotelConnector.PRIMARY_CMD, (byte) 0x8E, "usb", "usb"),
+    SOURCE_PHONO("Source Phono", PRIMARY_CMD, (byte) 0x35, "phono", "phono"),
+    SOURCE_USB("Source Front USB", PRIMARY_CMD, (byte) 0x8E, "usb", "usb"),
     SOURCE_PCUSB("Source PC USB", "pc_usb", "pcusb"),
-    SOURCE_MULTI_INPUT("Source Multi Input", RotelConnector.PRIMARY_CMD, (byte) 0x15, "multi_input", "multi_input"),
+    SOURCE_MULTI_INPUT("Source Multi Input", PRIMARY_CMD, (byte) 0x15, "multi_input", "multi_input"),
     SOURCE_AUX("Source Aux", "aux", "aux"),
     SOURCE_AUX1("Source Aux 1", "aux1", "aux1"),
     SOURCE_AUX2("Source Aux 2", "aux2", "aux2"),
@@ -111,126 +113,114 @@ public enum RotelCommand {
     SOURCE_IRADIO("Source iRadio", "iradio", "iradio"),
     SOURCE_NETWORK("Source Network", "network", "network"),
     SOURCE("Request current source", "get_current_source", "source?"),
-    MAIN_ZONE_SOURCE_CD("Main Zone Source CD", RotelConnector.MAIN_ZONE_CMD, (byte) 0x02, "main_zone_cd",
-            "main_zone_cd"),
-    MAIN_ZONE_SOURCE_TUNER("Main Zone Source Tuner", RotelConnector.MAIN_ZONE_CMD, (byte) 0x03, "main_zone_tuner",
-            "main_zone_tuner"),
-    MAIN_ZONE_SOURCE_TAPE("Main Zone Source Tape", RotelConnector.MAIN_ZONE_CMD, (byte) 0x04, "main_zone_tape",
-            "main_zone_tape"),
-    MAIN_ZONE_SOURCE_VIDEO1("Main Zone Source Video 1", RotelConnector.MAIN_ZONE_CMD, (byte) 0x05, "main_zone_video1",
+    MAIN_ZONE_SOURCE_CD("Main Zone Source CD", MAIN_ZONE_CMD, (byte) 0x02, "main_zone_cd", "main_zone_cd"),
+    MAIN_ZONE_SOURCE_TUNER("Main Zone Source Tuner", MAIN_ZONE_CMD, (byte) 0x03, "main_zone_tuner", "main_zone_tuner"),
+    MAIN_ZONE_SOURCE_TAPE("Main Zone Source Tape", MAIN_ZONE_CMD, (byte) 0x04, "main_zone_tape", "main_zone_tape"),
+    MAIN_ZONE_SOURCE_VIDEO1("Main Zone Source Video 1", MAIN_ZONE_CMD, (byte) 0x05, "main_zone_video1",
             "main_zone_video1"),
-    MAIN_ZONE_SOURCE_VIDEO2("Main Zone Source Video 2", RotelConnector.MAIN_ZONE_CMD, (byte) 0x06, "main_zone_video2",
+    MAIN_ZONE_SOURCE_VIDEO2("Main Zone Source Video 2", MAIN_ZONE_CMD, (byte) 0x06, "main_zone_video2",
             "main_zone_video2"),
-    MAIN_ZONE_SOURCE_VIDEO3("Main Zone Source Video 3", RotelConnector.MAIN_ZONE_CMD, (byte) 0x07, "main_zone_video3",
+    MAIN_ZONE_SOURCE_VIDEO3("Main Zone Source Video 3", MAIN_ZONE_CMD, (byte) 0x07, "main_zone_video3",
             "main_zone_video3"),
-    MAIN_ZONE_SOURCE_VIDEO4("Main Zone Source Video 4", RotelConnector.MAIN_ZONE_CMD, (byte) 0x08, "main_zone_video4",
+    MAIN_ZONE_SOURCE_VIDEO4("Main Zone Source Video 4", MAIN_ZONE_CMD, (byte) 0x08, "main_zone_video4",
             "main_zone_video4"),
-    MAIN_ZONE_SOURCE_VIDEO5("Main Zone Source Video 5", RotelConnector.MAIN_ZONE_CMD, (byte) 0x09, "main_zone_video5",
+    MAIN_ZONE_SOURCE_VIDEO5("Main Zone Source Video 5", MAIN_ZONE_CMD, (byte) 0x09, "main_zone_video5",
             "main_zone_video5"),
-    MAIN_ZONE_SOURCE_VIDEO6("Main Zone Source Video 6", RotelConnector.MAIN_ZONE_CMD, (byte) 0x94, "main_zone_video6",
+    MAIN_ZONE_SOURCE_VIDEO6("Main Zone Source Video 6", MAIN_ZONE_CMD, (byte) 0x94, "main_zone_video6",
             "main_zone_video6"),
-    MAIN_ZONE_SOURCE_USB("Main Zone Source Front USB", RotelConnector.MAIN_ZONE_CMD, (byte) 0x8E, "main_zone_usb",
-            "main_zone_usb"),
-    MAIN_ZONE_SOURCE_MULTI_INPUT("Main Zone Source Multi Input", RotelConnector.MAIN_ZONE_CMD, (byte) 0x15,
-            "main_zone_multi_input", "main_zone_multi_input"),
-    RECORD_SOURCE_CD("Record Source CD", RotelConnector.RECORD_SRC_CMD, (byte) 0x02, "record_cd", "record_cd"),
-    RECORD_SOURCE_TUNER("Record Source Tuner", RotelConnector.RECORD_SRC_CMD, (byte) 0x03, "record_tuner",
-            "record_tuner"),
-    RECORD_SOURCE_TAPE("Record Source Tape", RotelConnector.RECORD_SRC_CMD, (byte) 0x04, "record_tape", "record_tape"),
-    RECORD_SOURCE_VIDEO1("Record Source Video 1", RotelConnector.RECORD_SRC_CMD, (byte) 0x05, "record_video1",
-            "record_video1"),
-    RECORD_SOURCE_VIDEO2("Record Source Video 2", RotelConnector.RECORD_SRC_CMD, (byte) 0x06, "record_video2",
-            "record_video2"),
-    RECORD_SOURCE_VIDEO3("Record Source Video 3", RotelConnector.RECORD_SRC_CMD, (byte) 0x07, "record_video3",
-            "record_video3"),
-    RECORD_SOURCE_VIDEO4("Record Source Video 4", RotelConnector.RECORD_SRC_CMD, (byte) 0x08, "record_video4",
-            "record_video4"),
-    RECORD_SOURCE_VIDEO5("Record Source Video 5", RotelConnector.RECORD_SRC_CMD, (byte) 0x09, "record_video5",
-            "record_video5"),
-    RECORD_SOURCE_VIDEO6("Record Source Video 6", RotelConnector.RECORD_SRC_CMD, (byte) 0x94, "record_video6",
-            "record_video6"),
-    RECORD_SOURCE_USB("Record Source Front USB", RotelConnector.RECORD_SRC_CMD, (byte) 0x8E, "record_usb",
-            "record_usb"),
-    RECORD_SOURCE_MAIN("Record Follow Main Zone Source", RotelConnector.RECORD_SRC_CMD, (byte) 0x6B,
-            "record_follow_main", "record_follow_main"),
-    ZONE2_SOURCE_CD("Zone 2 Source CD", RotelConnector.ZONE2_CMD, (byte) 0x02, "zone2_cd", "zone2_cd"),
-    ZONE2_SOURCE_TUNER("Zone 2 Source Tuner", RotelConnector.ZONE2_CMD, (byte) 0x03, "zone2_tuner", "zone2_tuner"),
-    ZONE2_SOURCE_TAPE("Zone 2 Source Tape", RotelConnector.ZONE2_CMD, (byte) 0x04, "zone2_tape", "zone2_tape"),
-    ZONE2_SOURCE_VIDEO1("Zone 2 Source Video 1", RotelConnector.ZONE2_CMD, (byte) 0x05, "zone2_video1", "zone2_video1"),
-    ZONE2_SOURCE_VIDEO2("Zone 2 Source Video 2", RotelConnector.ZONE2_CMD, (byte) 0x06, "zone2_video2", "zone2_video2"),
-    ZONE2_SOURCE_VIDEO3("Zone 2 Source Video 3", RotelConnector.ZONE2_CMD, (byte) 0x07, "zone2_video3", "zone2_video3"),
-    ZONE2_SOURCE_VIDEO4("Zone 2 Source Video 4", RotelConnector.ZONE2_CMD, (byte) 0x08, "zone2_video4", "zone2_video4"),
-    ZONE2_SOURCE_VIDEO5("Zone 2 Source Video 5", RotelConnector.ZONE2_CMD, (byte) 0x09, "zone2_video5", "zone2_video5"),
-    ZONE2_SOURCE_VIDEO6("Zone 2 Source Video 6", RotelConnector.ZONE2_CMD, (byte) 0x94, "zone2_video6", "zone2_video6"),
-    ZONE2_SOURCE_USB("Zone 2 Source Front USB", RotelConnector.ZONE2_CMD, (byte) 0x8E, "zone2_usb", "zone2_usb"),
-    ZONE2_SOURCE_MAIN("Zone 2 Follow Main Zone Source", RotelConnector.ZONE2_CMD, (byte) 0x6B, "zone2_follow_main",
+    MAIN_ZONE_SOURCE_USB("Main Zone Source Front USB", MAIN_ZONE_CMD, (byte) 0x8E, "main_zone_usb", "main_zone_usb"),
+    MAIN_ZONE_SOURCE_MULTI_INPUT("Main Zone Source Multi Input", MAIN_ZONE_CMD, (byte) 0x15, "main_zone_multi_input",
+            "main_zone_multi_input"),
+    RECORD_SOURCE_CD("Record Source CD", RECORD_SRC_CMD, (byte) 0x02, "record_cd", "record_cd"),
+    RECORD_SOURCE_TUNER("Record Source Tuner", RECORD_SRC_CMD, (byte) 0x03, "record_tuner", "record_tuner"),
+    RECORD_SOURCE_TAPE("Record Source Tape", RECORD_SRC_CMD, (byte) 0x04, "record_tape", "record_tape"),
+    RECORD_SOURCE_VIDEO1("Record Source Video 1", RECORD_SRC_CMD, (byte) 0x05, "record_video1", "record_video1"),
+    RECORD_SOURCE_VIDEO2("Record Source Video 2", RECORD_SRC_CMD, (byte) 0x06, "record_video2", "record_video2"),
+    RECORD_SOURCE_VIDEO3("Record Source Video 3", RECORD_SRC_CMD, (byte) 0x07, "record_video3", "record_video3"),
+    RECORD_SOURCE_VIDEO4("Record Source Video 4", RECORD_SRC_CMD, (byte) 0x08, "record_video4", "record_video4"),
+    RECORD_SOURCE_VIDEO5("Record Source Video 5", RECORD_SRC_CMD, (byte) 0x09, "record_video5", "record_video5"),
+    RECORD_SOURCE_VIDEO6("Record Source Video 6", RECORD_SRC_CMD, (byte) 0x94, "record_video6", "record_video6"),
+    RECORD_SOURCE_USB("Record Source Front USB", RECORD_SRC_CMD, (byte) 0x8E, "record_usb", "record_usb"),
+    RECORD_SOURCE_MAIN("Record Follow Main Zone Source", RECORD_SRC_CMD, (byte) 0x6B, "record_follow_main",
+            "record_follow_main"),
+    ZONE2_SOURCE_CD("Zone 2 Source CD", ZONE2_CMD, (byte) 0x02, "zone2_cd", "zone2_cd"),
+    ZONE2_SOURCE_TUNER("Zone 2 Source Tuner", ZONE2_CMD, (byte) 0x03, "zone2_tuner", "zone2_tuner"),
+    ZONE2_SOURCE_TAPE("Zone 2 Source Tape", ZONE2_CMD, (byte) 0x04, "zone2_tape", "zone2_tape"),
+    ZONE2_SOURCE_VIDEO1("Zone 2 Source Video 1", ZONE2_CMD, (byte) 0x05, "zone2_video1", "zone2_video1"),
+    ZONE2_SOURCE_VIDEO2("Zone 2 Source Video 2", ZONE2_CMD, (byte) 0x06, "zone2_video2", "zone2_video2"),
+    ZONE2_SOURCE_VIDEO3("Zone 2 Source Video 3", ZONE2_CMD, (byte) 0x07, "zone2_video3", "zone2_video3"),
+    ZONE2_SOURCE_VIDEO4("Zone 2 Source Video 4", ZONE2_CMD, (byte) 0x08, "zone2_video4", "zone2_video4"),
+    ZONE2_SOURCE_VIDEO5("Zone 2 Source Video 5", ZONE2_CMD, (byte) 0x09, "zone2_video5", "zone2_video5"),
+    ZONE2_SOURCE_VIDEO6("Zone 2 Source Video 6", ZONE2_CMD, (byte) 0x94, "zone2_video6", "zone2_video6"),
+    ZONE2_SOURCE_USB("Zone 2 Source Front USB", ZONE2_CMD, (byte) 0x8E, "zone2_usb", "zone2_usb"),
+    ZONE2_SOURCE_MAIN("Zone 2 Follow Main Zone Source", ZONE2_CMD, (byte) 0x6B, "zone2_follow_main",
             "zone2_follow_main"),
-    ZONE3_SOURCE_CD("Zone 3 Source CD", RotelConnector.ZONE3_CMD, (byte) 0x02, "zone3_cd", "zone3_cd"),
-    ZONE3_SOURCE_TUNER("Zone 3 Source Tuner", RotelConnector.ZONE3_CMD, (byte) 0x03, "zone3_tuner", "zone3_tuner"),
-    ZONE3_SOURCE_TAPE("Zone 3 Source Tape", RotelConnector.ZONE3_CMD, (byte) 0x04, "zone3_tape", "zone3_tape"),
-    ZONE3_SOURCE_VIDEO1("Zone 3 Source Video 1", RotelConnector.ZONE3_CMD, (byte) 0x05, "zone3_video1", "zone3_video1"),
-    ZONE3_SOURCE_VIDEO2("Zone 3 Source Video 2", RotelConnector.ZONE3_CMD, (byte) 0x06, "zone3_video2", "zone3_video2"),
-    ZONE3_SOURCE_VIDEO3("Zone 3 Source Video 3", RotelConnector.ZONE3_CMD, (byte) 0x07, "zone3_video3", "zone3_video3"),
-    ZONE3_SOURCE_VIDEO4("Zone 3 Source Video 4", RotelConnector.ZONE3_CMD, (byte) 0x08, "zone3_video4", "zone3_video4"),
-    ZONE3_SOURCE_VIDEO5("Zone 3 Source Video 5", RotelConnector.ZONE3_CMD, (byte) 0x09, "zone3_video5", "zone3_video5"),
-    ZONE3_SOURCE_VIDEO6("Zone 3 Source Video 6", RotelConnector.ZONE3_CMD, (byte) 0x94, "zone3_video6", "zone3_video6"),
-    ZONE3_SOURCE_USB("Zone 3 Source Front USB", RotelConnector.ZONE3_CMD, (byte) 0x8E, "zone3_usb", "zone3_usb"),
-    ZONE3_SOURCE_MAIN("Zone 3 Follow Main Zone Source", RotelConnector.ZONE3_CMD, (byte) 0x6B, "zone3_follow_main",
+    ZONE3_SOURCE_CD("Zone 3 Source CD", ZONE3_CMD, (byte) 0x02, "zone3_cd", "zone3_cd"),
+    ZONE3_SOURCE_TUNER("Zone 3 Source Tuner", ZONE3_CMD, (byte) 0x03, "zone3_tuner", "zone3_tuner"),
+    ZONE3_SOURCE_TAPE("Zone 3 Source Tape", ZONE3_CMD, (byte) 0x04, "zone3_tape", "zone3_tape"),
+    ZONE3_SOURCE_VIDEO1("Zone 3 Source Video 1", ZONE3_CMD, (byte) 0x05, "zone3_video1", "zone3_video1"),
+    ZONE3_SOURCE_VIDEO2("Zone 3 Source Video 2", ZONE3_CMD, (byte) 0x06, "zone3_video2", "zone3_video2"),
+    ZONE3_SOURCE_VIDEO3("Zone 3 Source Video 3", ZONE3_CMD, (byte) 0x07, "zone3_video3", "zone3_video3"),
+    ZONE3_SOURCE_VIDEO4("Zone 3 Source Video 4", ZONE3_CMD, (byte) 0x08, "zone3_video4", "zone3_video4"),
+    ZONE3_SOURCE_VIDEO5("Zone 3 Source Video 5", ZONE3_CMD, (byte) 0x09, "zone3_video5", "zone3_video5"),
+    ZONE3_SOURCE_VIDEO6("Zone 3 Source Video 6", ZONE3_CMD, (byte) 0x94, "zone3_video6", "zone3_video6"),
+    ZONE3_SOURCE_USB("Zone 3 Source Front USB", ZONE3_CMD, (byte) 0x8E, "zone3_usb", "zone3_usb"),
+    ZONE3_SOURCE_MAIN("Zone 3 Follow Main Zone Source", ZONE3_CMD, (byte) 0x6B, "zone3_follow_main",
             "zone3_follow_main"),
-    ZONE4_SOURCE_CD("Zone 4 Source CD", RotelConnector.ZONE4_CMD, (byte) 0x02, "zone4_cd", "zone4_cd"),
-    ZONE4_SOURCE_TUNER("Zone 4 Source Tuner", RotelConnector.ZONE4_CMD, (byte) 0x03, "zone4_tuner", "zone4_tuner"),
-    ZONE4_SOURCE_TAPE("Zone 4 Source Tape", RotelConnector.ZONE4_CMD, (byte) 0x04, "zone4_tape", "zone4_tape"),
-    ZONE4_SOURCE_VIDEO1("Zone 4 Source Video 1", RotelConnector.ZONE4_CMD, (byte) 0x05, "zone4_video1", "zone4_video1"),
-    ZONE4_SOURCE_VIDEO2("Zone 4 Source Video 2", RotelConnector.ZONE4_CMD, (byte) 0x06, "zone4_video2", "zone4_video2"),
-    ZONE4_SOURCE_VIDEO3("Zone 4 Source Video 3", RotelConnector.ZONE4_CMD, (byte) 0x07, "zone4_video3", "zone4_video3"),
-    ZONE4_SOURCE_VIDEO4("Zone 4 Source Video 4", RotelConnector.ZONE4_CMD, (byte) 0x08, "zone4_video4", "zone4_video4"),
-    ZONE4_SOURCE_VIDEO5("Zone 4 Source Video 5", RotelConnector.ZONE4_CMD, (byte) 0x09, "zone4_video5", "zone4_video5"),
-    ZONE4_SOURCE_VIDEO6("Zone 4 Source Video 6", RotelConnector.ZONE4_CMD, (byte) 0x94, "zone4_video6", "zone4_video6"),
-    ZONE4_SOURCE_USB("Zone 4 Source Front USB", RotelConnector.ZONE4_CMD, (byte) 0x8E, "zone4_usb", "zone4_usb"),
-    ZONE4_SOURCE_MAIN("Zone 4 Follow Main Zone Source", RotelConnector.ZONE4_CMD, (byte) 0x6B, "zone4_follow_main",
+    ZONE4_SOURCE_CD("Zone 4 Source CD", ZONE4_CMD, (byte) 0x02, "zone4_cd", "zone4_cd"),
+    ZONE4_SOURCE_TUNER("Zone 4 Source Tuner", ZONE4_CMD, (byte) 0x03, "zone4_tuner", "zone4_tuner"),
+    ZONE4_SOURCE_TAPE("Zone 4 Source Tape", ZONE4_CMD, (byte) 0x04, "zone4_tape", "zone4_tape"),
+    ZONE4_SOURCE_VIDEO1("Zone 4 Source Video 1", ZONE4_CMD, (byte) 0x05, "zone4_video1", "zone4_video1"),
+    ZONE4_SOURCE_VIDEO2("Zone 4 Source Video 2", ZONE4_CMD, (byte) 0x06, "zone4_video2", "zone4_video2"),
+    ZONE4_SOURCE_VIDEO3("Zone 4 Source Video 3", ZONE4_CMD, (byte) 0x07, "zone4_video3", "zone4_video3"),
+    ZONE4_SOURCE_VIDEO4("Zone 4 Source Video 4", ZONE4_CMD, (byte) 0x08, "zone4_video4", "zone4_video4"),
+    ZONE4_SOURCE_VIDEO5("Zone 4 Source Video 5", ZONE4_CMD, (byte) 0x09, "zone4_video5", "zone4_video5"),
+    ZONE4_SOURCE_VIDEO6("Zone 4 Source Video 6", ZONE4_CMD, (byte) 0x94, "zone4_video6", "zone4_video6"),
+    ZONE4_SOURCE_USB("Zone 4 Source Front USB", ZONE4_CMD, (byte) 0x8E, "zone4_usb", "zone4_usb"),
+    ZONE4_SOURCE_MAIN("Zone 4 Follow Main Zone Source", ZONE4_CMD, (byte) 0x6B, "zone4_follow_main",
             "zone4_follow_main"),
-    STEREO("Stereo", RotelConnector.PRIMARY_CMD, (byte) 0x11, "2channel", "2channel"),
-    STEREO3("Dolby 3 Stereo ", RotelConnector.PRIMARY_CMD, (byte) 0x12, "3channel", "3channel"),
-    STEREO5("5 Channel Stereo", RotelConnector.PRIMARY_CMD, (byte) 0x5B, "5channel", "5channel"),
-    STEREO7("7 Channel Stereo", RotelConnector.PRIMARY_CMD, (byte) 0x5C, "7channel", "7channel"),
+    STEREO("Stereo", PRIMARY_CMD, (byte) 0x11, "2channel", "2channel"),
+    STEREO3("Dolby 3 Stereo ", PRIMARY_CMD, (byte) 0x12, "3channel", "3channel"),
+    STEREO5("5 Channel Stereo", PRIMARY_CMD, (byte) 0x5B, "5channel", "5channel"),
+    STEREO7("7 Channel Stereo", PRIMARY_CMD, (byte) 0x5C, "7channel", "7channel"),
     STEREO9("9 Channel Stereo", "9channel", "9channel"),
     STEREO11("11 Channel Stereo", "11channel", "11channel"),
-    DSP1("DSP 1", RotelConnector.PRIMARY_CMD, (byte) 0x57),
-    DSP2("DSP 2", RotelConnector.PRIMARY_CMD, (byte) 0x58),
-    DSP3("DSP 3", RotelConnector.PRIMARY_CMD, (byte) 0x59),
-    DSP4("DSP 4", RotelConnector.PRIMARY_CMD, (byte) 0x5A),
-    PROLOGIC("Dolby Pro Logic", RotelConnector.PRIMARY_CMD, (byte) 0x5F),
-    PLII_CINEMA("Dolby PLII Cinema", RotelConnector.PRIMARY_CMD, (byte) 0x5D, "prologic_movie", "prologic_movie"),
-    PLII_MUSIC("Dolby PLII Music", RotelConnector.PRIMARY_CMD, (byte) 0x5E, "prologic_music", "prologic_music"),
-    PLII_GAME("Dolby PLII Game", RotelConnector.PRIMARY_CMD, (byte) 0x74, "prologic_game", "prologic_game"),
-    PLIIZ("Dolby PLIIz", RotelConnector.PRIMARY_CMD, (byte) 0x92, "prologic_iiz", "prologic_iiz"),
-    NEO6_MUSIC("dts Neo:6 Music", RotelConnector.PRIMARY_CMD, (byte) 0x60, "neo6_music", "neo6_music"),
-    NEO6_CINEMA("dts Neo:6 Cinema", RotelConnector.PRIMARY_CMD, (byte) 0x61, "neo6_cinema", "neo6_cinema"),
+    DSP1("DSP 1", PRIMARY_CMD, (byte) 0x57),
+    DSP2("DSP 2", PRIMARY_CMD, (byte) 0x58),
+    DSP3("DSP 3", PRIMARY_CMD, (byte) 0x59),
+    DSP4("DSP 4", PRIMARY_CMD, (byte) 0x5A),
+    PROLOGIC("Dolby Pro Logic", PRIMARY_CMD, (byte) 0x5F),
+    PLII_CINEMA("Dolby PLII Cinema", PRIMARY_CMD, (byte) 0x5D, "prologic_movie", "prologic_movie"),
+    PLII_MUSIC("Dolby PLII Music", PRIMARY_CMD, (byte) 0x5E, "prologic_music", "prologic_music"),
+    PLII_GAME("Dolby PLII Game", PRIMARY_CMD, (byte) 0x74, "prologic_game", "prologic_game"),
+    PLIIZ("Dolby PLIIz", PRIMARY_CMD, (byte) 0x92, "prologic_iiz", "prologic_iiz"),
+    NEO6_MUSIC("dts Neo:6 Music", PRIMARY_CMD, (byte) 0x60, "neo6_music", "neo6_music"),
+    NEO6_CINEMA("dts Neo:6 Cinema", PRIMARY_CMD, (byte) 0x61, "neo6_cinema", "neo6_cinema"),
     ATMOS("Dolby Atmos", "dolby_atmos", "dolby_atmos"),
     NEURAL_X("dts Neural:X", "dts_neural", "dts_neural"),
-    BYPASS("Analog Bypass", RotelConnector.PRIMARY_CMD, (byte) 0x11, "bypass", "bypass"),
+    BYPASS("Analog Bypass", PRIMARY_CMD, (byte) 0x11, "bypass", "bypass"),
     DSP_MODE("Request current DSP mode", "get_dsp_mode", "dsp_mode"),
     TONE_MAX("Request Max tone level", "get_tone_max", null),
-    TONE_CONTROL_SELECT("Tone Control Select", RotelConnector.PRIMARY_CMD, (byte) 0x67),
-    TREBLE_UP("Treble Up", RotelConnector.PRIMARY_CMD, (byte) 0x0D, "treble_up", "treble_up"),
-    TREBLE_DOWN("Treble Down", RotelConnector.PRIMARY_CMD, (byte) 0x0E, "treble_down", "treble_down"),
+    TONE_CONTROL_SELECT("Tone Control Select", PRIMARY_CMD, (byte) 0x67),
+    TREBLE_UP("Treble Up", PRIMARY_CMD, (byte) 0x0D, "treble_up", "treble_up"),
+    TREBLE_DOWN("Treble Down", PRIMARY_CMD, (byte) 0x0E, "treble_down", "treble_down"),
     TREBLE_SET("Set Treble to level", "treble_", "treble_"),
     TREBLE("Request current treble level", "get_treble", "treble?"),
-    BASS_UP("Bass Up", RotelConnector.PRIMARY_CMD, (byte) 0x0F, "bass_up", "bass_up"),
-    BASS_DOWN("Bass Down", RotelConnector.PRIMARY_CMD, (byte) 0x10, "bass_down", "bass_down"),
+    BASS_UP("Bass Up", PRIMARY_CMD, (byte) 0x0F, "bass_up", "bass_up"),
+    BASS_DOWN("Bass Down", PRIMARY_CMD, (byte) 0x10, "bass_down", "bass_down"),
     BASS_SET("Set Bass to level", "bass_", "bass_"),
     BASS("Request current bass level", "get_bass", "bass?"),
-    RECORD_FONCTION_SELECT("Record Function Select", RotelConnector.PRIMARY_CMD, (byte) 0x17),
-    PLAY("Play Source", RotelConnector.PRIMARY_CMD, (byte) 0x04, "play", "play"),
-    STOP("Stop Source", RotelConnector.PRIMARY_CMD, (byte) 0x06, "stop", "stop"),
-    PAUSE("Pause Source", RotelConnector.PRIMARY_CMD, (byte) 0x05, "pause", "pause"),
+    RECORD_FONCTION_SELECT("Record Function Select", PRIMARY_CMD, (byte) 0x17),
+    PLAY("Play Source", PRIMARY_CMD, (byte) 0x04, "play", "play"),
+    STOP("Stop Source", PRIMARY_CMD, (byte) 0x06, "stop", "stop"),
+    PAUSE("Pause Source", PRIMARY_CMD, (byte) 0x05, "pause", "pause"),
     CD_PLAY_STATUS("Request CD play status", "get_cd_play_status", null),
     PLAY_STATUS("Request source play status", "get_play_status", "status"),
-    TRACK_FORWARD("Track Forward", RotelConnector.PRIMARY_CMD, (byte) 0x09, "track_fwd", "trkf"),
-    TRACK_BACKWORD("Track Backward", RotelConnector.PRIMARY_CMD, (byte) 0x08, "track_back", "trkb"),
+    TRACK_FORWARD("Track Forward", PRIMARY_CMD, (byte) 0x09, "track_fwd", "trkf"),
+    TRACK_BACKWORD("Track Backward", PRIMARY_CMD, (byte) 0x08, "track_back", "trkb"),
     TRACK("Request current CD track number", null, "track"),
     FREQUENCY("Request current frequency for digital source input", "get_current_freq", "freq?"),
-    DISPLAY_REFRESH("Display Refresh", RotelConnector.PRIMARY_CMD, (byte) 0xFF),
+    DISPLAY_REFRESH("Display Refresh", PRIMARY_CMD, (byte) 0xFF),
     DIMMER_LEVEL_GET("Request current front display dimmer level", "get_current_dimmer", "dimmer?"),
     DIMMER_LEVEL_SET("Set front display dimmer to level", "dimmer_", "dimmer_"),
     UPDATE_AUTO("Set Update to Auto", "display_update_auto", "rs232_update_on"),
@@ -245,10 +235,10 @@ public enum RotelCommand {
     BALANCE_LEFT("Balance Left", "balance_left", "balance_l"),
     BALANCE_SET("Set Balance to level", "balance_", "balance_"),
     BALANCE("Request current balance setting", "get_balance", "balance?"),
-    SPEAKER_A_TOGGLE("Toggle Speaker A Output", RotelConnector.PRIMARY_CMD, (byte) 0x50, "speaker_a", "speaker_a"),
+    SPEAKER_A_TOGGLE("Toggle Speaker A Output", PRIMARY_CMD, (byte) 0x50, "speaker_a", "speaker_a"),
     SPEAKER_A_ON("Set Speaker A Output", "speaker_a_on", "speaker_a_on"),
     SPEAKER_A_OFF("Unset Speaker A Output", "speaker_a_off", "speaker_a_off"),
-    SPEAKER_B_TOGGLE("Toggle Speaker B Output", RotelConnector.PRIMARY_CMD, (byte) 0x51, "speaker_b", "speaker_b"),
+    SPEAKER_B_TOGGLE("Toggle Speaker B Output", PRIMARY_CMD, (byte) 0x51, "speaker_b", "speaker_b"),
     SPEAKER_B_ON("Set Speaker B Output", "speaker_b_on", "speaker_b_on"),
     SPEAKER_B_OFF("Unset Speaker B Output", "speaker_b_off", "speaker_b_off"),
     SPEAKER("Request current active speaker outputs", "get_current_speaker", "speaker?");
index 06fbdd916b287ee433dc30f6793e786daf660a3f..0849ac4a781a526312c1360ec542ac901126bc39 100644 (file)
@@ -16,18 +16,11 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.PatternSyntaxException;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.rotel.internal.RotelException;
-import org.openhab.binding.rotel.internal.RotelModel;
-import org.openhab.core.util.HexUtils;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,148 +34,8 @@ public abstract class RotelConnector {
 
     private final Logger logger = LoggerFactory.getLogger(RotelConnector.class);
 
-    public static final byte[] READ_ERROR = "read_error".getBytes(StandardCharsets.US_ASCII);
-
-    protected static final byte START = (byte) 0xFE;
-
-    // Message types
-    public static final byte PRIMARY_CMD = (byte) 0x10;
-    public static final byte MAIN_ZONE_CMD = (byte) 0x14;
-    public static final byte RECORD_SRC_CMD = (byte) 0x15;
-    public static final byte ZONE2_CMD = (byte) 0x16;
-    public static final byte ZONE3_CMD = (byte) 0x17;
-    public static final byte ZONE4_CMD = (byte) 0x18;
-    public static final byte VOLUME_CMD = (byte) 0x30;
-    public static final byte ZONE2_VOLUME_CMD = (byte) 0x32;
-    public static final byte ZONE3_VOLUME_CMD = (byte) 0x33;
-    public static final byte ZONE4_VOLUME_CMD = (byte) 0x34;
-    private static final byte TRIGGER_CMD = (byte) 0x40;
-    protected static final byte STANDARD_RESPONSE = (byte) 0x20;
-    private static final byte TRIGGER_STATUS = (byte) 0x21;
-    private static final byte SMART_DISPLAY_DATA_1 = (byte) 0x22;
-    private static final byte SMART_DISPLAY_DATA_2 = (byte) 0x23;
-
-    // Keys used by the HEX protocol
-    private static final String KEY1_HEX_VOLUME = "volume ";
-    private static final String KEY2_HEX_VOLUME = "vol ";
-    private static final String KEY_HEX_MUTE = "mute ";
-    private static final String KEY1_HEX_BASS = "bass ";
-    private static final String KEY2_HEX_BASS = "lf ";
-    private static final String KEY1_HEX_TREBLE = "treble ";
-    private static final String KEY2_HEX_TREBLE = "hf ";
-    private static final String KEY_HEX_MULTI_IN = "multi in ";
-    private static final String KEY_HEX_STEREO = "stereo";
-    private static final String KEY1_HEX_3CH = "3 stereo";
-    private static final String KEY2_HEX_3CH = "dolby 3 stereo";
-    private static final String KEY_HEX_5CH = "5ch stereo";
-    private static final String KEY_HEX_7CH = "7ch stereo";
-    private static final String KEY_HEX_MUSIC1 = "music 1";
-    private static final String KEY_HEX_MUSIC2 = "music 2";
-    private static final String KEY_HEX_MUSIC3 = "music 3";
-    private static final String KEY_HEX_MUSIC4 = "music 4";
-    private static final String KEY_HEX_DSP1 = "dsp 1";
-    private static final String KEY_HEX_DSP2 = "dsp 2";
-    private static final String KEY_HEX_DSP3 = "dsp 3";
-    private static final String KEY_HEX_DSP4 = "dsp 4";
-    private static final String KEY1_HEX_PROLOGIC = "prologic  emu";
-    private static final String KEY2_HEX_PROLOGIC = "dolby pro logic";
-    private static final String KEY1_HEX_PLII_CINEMA = "prologic  cin";
-    private static final String KEY2_HEX_PLII_CINEMA = "dolby pl  c";
-    private static final String KEY1_HEX_PLII_MUSIC = "prologic  mus";
-    private static final String KEY2_HEX_PLII_MUSIC = "dolby pl  m";
-    private static final String KEY1_HEX_PLII_GAME = "prologic  gam";
-    private static final String KEY2_HEX_PLII_GAME = "dolby pl  g";
-    private static final String KEY1_HEX_PLIIX_CINEMA = "pl x cinema";
-    private static final String KEY2_HEX_PLIIX_CINEMA = "dolby pl x c";
-    private static final String KEY1_HEX_PLIIX_MUSIC = "pl x music";
-    private static final String KEY2_HEX_PLIIX_MUSIC = "dolby pl x m";
-    private static final String KEY1_HEX_PLIIX_GAME = "pl x game";
-    private static final String KEY2_HEX_PLIIX_GAME = "dolby pl x g";
-    private static final String KEY_HEX_PLIIZ = "dolby pl z";
-    private static final String KEY1_HEX_DTS_NEO6_CINEMA = "neo 6 cinema";
-    private static final String KEY2_HEX_DTS_NEO6_CINEMA = "dts neo:6 c";
-    private static final String KEY1_HEX_DTS_NEO6_MUSIC = "neo 6 music";
-    private static final String KEY2_HEX_DTS_NEO6_MUSIC = "dts neo:6 m";
-    private static final String KEY_HEX_DTS = "dts";
-    private static final String KEY_HEX_DTS_ES = "dts-es";
-    private static final String KEY_HEX_DTS_96 = "dts 96";
-    private static final String KEY_HEX_DD = "dolby digital";
-    private static final String KEY_HEX_DD_EX = "dolby d ex";
-    private static final String KEY_HEX_PCM = "pcm";
-    private static final String KEY_HEX_LPCM = "lpcm";
-    private static final String KEY_HEX_MPEG = "mpeg";
-    private static final String KEY_HEX_BYPASS = "bypass";
-    private static final String KEY1_HEX_ZONE2 = "zone ";
-    private static final String KEY2_HEX_ZONE2 = "zone2 ";
-    private static final String KEY_HEX_ZONE3 = "zone3 ";
-    private static final String KEY_HEX_ZONE4 = "zone4 ";
-    private static final String KEY_HEX_RECORD = "rec ";
-
-    // Keys used by the ASCII protocol
-    public static final String KEY_UPDATE_MODE = "update_mode";
-    public static final String KEY_DISPLAY_UPDATE = "display_update";
-    public static final String KEY_POWER = "power";
-    public static final String KEY_VOLUME_MIN = "volume_min";
-    public static final String KEY_VOLUME_MAX = "volume_max";
-    public static final String KEY_VOLUME = "volume";
-    public static final String KEY_MUTE = "mute";
-    public static final String KEY_TONE_MAX = "tone_max";
-    public static final String KEY_BASS = "bass";
-    public static final String KEY_TREBLE = "treble";
-    public static final String KEY_SOURCE = "source";
-    public static final String KEY1_PLAY_STATUS = "play_status";
-    public static final String KEY2_PLAY_STATUS = "status";
-    public static final String KEY_TRACK = "track";
-    public static final String KEY_DSP_MODE = "dsp_mode";
-    public static final String KEY_DIMMER = "dimmer";
-    public static final String KEY_FREQ = "freq";
-    public static final String KEY_TONE = "tone";
-    public static final String KEY_TCBYPASS = "bypass";
-    public static final String KEY_BALANCE = "balance";
-    public static final String KEY_SPEAKER = "speaker";
-
-    // Special keys used by the binding
-    public static final String KEY_LINE1 = "line1";
-    public static final String KEY_LINE2 = "line2";
-    public static final String KEY_RECORD = "record";
-    public static final String KEY_RECORD_SEL = "record_sel";
-    public static final String KEY_ZONE = "zone";
-    public static final String KEY_POWER_ZONE2 = "power_zone2";
-    public static final String KEY_POWER_ZONE3 = "power_zone3";
-    public static final String KEY_POWER_ZONE4 = "power_zone4";
-    public static final String KEY_SOURCE_ZONE2 = "source_zone2";
-    public static final String KEY_SOURCE_ZONE3 = "source_zone3";
-    public static final String KEY_SOURCE_ZONE4 = "source_zone4";
-    public static final String KEY_VOLUME_ZONE2 = "volume_zone2";
-    public static final String KEY_VOLUME_ZONE3 = "volume_zone3";
-    public static final String KEY_VOLUME_ZONE4 = "volume_zone4";
-    public static final String KEY_MUTE_ZONE2 = "mute_zone2";
-    public static final String KEY_MUTE_ZONE3 = "mute_zone3";
-    public static final String KEY_MUTE_ZONE4 = "mute_zone4";
-    public static final String KEY_ERROR = "error";
-
-    public static final String MSG_VALUE_OFF = "off";
-    public static final String MSG_VALUE_ON = "on";
-    public static final String POWER_ON = "on";
-    public static final String STANDBY = "standby";
-    public static final String POWER_OFF_DELAYED = "off_delayed";
-    protected static final String AUTO = "auto";
-    protected static final String MANUAL = "manual";
-    public static final String MSG_VALUE_MIN = "min";
-    public static final String MSG_VALUE_MAX = "max";
-    public static final String MSG_VALUE_FIX = "fix";
-    public static final String PLAY = "play";
-    public static final String PAUSE = "pause";
-    public static final String STOP = "stop";
-    private static final String SOURCE = "source";
-    public static final String MSG_VALUE_SPEAKER_A = "a";
-    public static final String MSG_VALUE_SPEAKER_B = "b";
-    public static final String MSG_VALUE_SPEAKER_AB = "a_b";
-
-    private RotelModel model;
-    private RotelProtocol protocol;
-    protected Map<RotelSource, String> sourcesLabels;
-    private boolean simu;
+    private final boolean simu;
+    protected final Thread readerThread;
 
     /** The output stream */
     protected @Nullable OutputStream dataOut;
@@ -193,69 +46,16 @@ public abstract class RotelConnector {
     /** true if the connection is established, false if not */
     private boolean connected;
 
-    protected String readerThreadName;
-    private @Nullable Thread readerThread;
-
-    private List<RotelMessageEventListener> listeners = new ArrayList<>();
-
-    /** Special characters that can be found in the feedback messages for several devices using the ASCII protocol */
-    public static final byte[][] SPECIAL_CHARACTERS = { { (byte) 0xEE, (byte) 0x82, (byte) 0x85 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x84 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x92 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x87 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8E },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x89 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x93 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x8C }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8F },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x8A }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8B },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x81 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x82 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x83 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x94 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x97 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x98 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x80 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x99 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x9A }, { (byte) 0xEE, (byte) 0x82, (byte) 0x88 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x95 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x96 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x90 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x91 },
-            { (byte) 0xEE, (byte) 0x82, (byte) 0x8D }, { (byte) 0xEE, (byte) 0x80, (byte) 0x80, (byte) 0xEE,
-                    (byte) 0x80, (byte) 0x81, (byte) 0xEE, (byte) 0x80, (byte) 0x82 } };
-
-    /** Special characters that can be found in the feedback messages for the RCD-1572 */
-    public static final byte[][] SPECIAL_CHARACTERS_RCD1572 = { { (byte) 0xC2, (byte) 0x8C },
-            { (byte) 0xC2, (byte) 0x54 }, { (byte) 0xC2, (byte) 0x81 }, { (byte) 0xC2, (byte) 0x82 },
-            { (byte) 0xC2, (byte) 0x83 } };
-
-    /** Empty table of special characters */
-    public static final byte[][] NO_SPECIAL_CHARACTERS = {};
-
     /**
      * Constructor
      *
-     * @param model the Rotel model in use
-     * @param protocol the protocol to be used
+     * @param protocolHandler the protocol handler
      * @param simu whether the communication is simulated or real
      * @param readerThreadName the name of thread to be created
      */
-    public RotelConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
-            boolean simu, String readerThreadName) {
-        this.model = model;
-        this.protocol = protocol;
-        this.sourcesLabels = sourcesLabels;
+    public RotelConnector(RotelAbstractProtocolHandler protocolHandler, boolean simu, String readerThreadName) {
         this.simu = simu;
-        this.readerThreadName = readerThreadName;
-    }
-
-    /**
-     * Get the Rotel model
-     *
-     * @return the model
-     */
-    public RotelModel getModel() {
-        return model;
-    }
-
-    /**
-     * Get the protocol to be used
-     *
-     * @return the protocol
-     */
-    public RotelProtocol getProtocol() {
-        return protocol;
+        this.readerThread = new RotelReaderThread(this, protocolHandler, readerThreadName);
     }
 
     /**
@@ -276,15 +76,6 @@ public abstract class RotelConnector {
         this.connected = connected;
     }
 
-    /**
-     * Set the thread that handles the feedback messages
-     *
-     * @param readerThread the thread
-     */
-    protected void setReaderThread(Thread readerThread) {
-        this.readerThread = readerThread;
-    }
-
     /**
      * Open the connection with the Rotel device
      *
@@ -301,14 +92,10 @@ public abstract class RotelConnector {
      * Stop the thread that handles the feedback messages and close the opened input and output streams
      */
     protected void cleanup() {
-        Thread readerThread = this.readerThread;
-        if (readerThread != null) {
-            readerThread.interrupt();
-            try {
-                readerThread.join();
-            } catch (InterruptedException e) {
-            }
-            this.readerThread = null;
+        readerThread.interrupt();
+        try {
+            readerThread.join();
+        } catch (InterruptedException e) {
         }
         OutputStream dataOut = this.dataOut;
         if (dataOut != null) {
@@ -361,890 +148,26 @@ public abstract class RotelConnector {
     /**
      * Request the Rotel device to execute a command
      *
-     * @param cmd the command to execute
-     *
-     * @throws RotelException - In case of any problem
-     */
-    public void sendCommand(RotelCommand cmd) throws RotelException {
-        sendCommand(cmd, null);
-    }
-
-    /**
-     * Request the Rotel device to execute a command
-     *
-     * @param cmd the command to execute
-     * @param value the integer value to consider for volume, bass or treble adjustment
+     * @param cmdName the command name
+     * @param dataBuffer the data buffer containing the encoded command
      *
      * @throws RotelException - In case of any problem
      */
-    public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
-        String messageStr;
-        byte[] message = new byte[0];
-        switch (protocol) {
-            case HEX:
-                if (cmd.getHexType() == 0) {
-                    logger.debug("Send comman \"{}\" ignored: not available for HEX protocol", cmd.getName());
-                    return;
-                } else {
-                    final int size = 6;
-                    message = new byte[size];
-                    int idx = 0;
-                    message[idx++] = START;
-                    message[idx++] = 3;
-                    message[idx++] = model.getDeviceId();
-                    message[idx++] = cmd.getHexType();
-                    message[idx++] = (value == null) ? cmd.getHexKey() : (byte) (value & 0x000000FF);
-                    final byte checksum = computeCheckSum(message, idx - 1);
-                    if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
-                        message = Arrays.copyOf(message, size + 1);
-                        message[idx++] = (byte) 0xFD;
-                        message[idx++] = ((checksum & 0x000000FF) == 0x000000FD) ? (byte) 0 : (byte) 1;
-                    } else {
-                        message[idx++] = checksum;
-                    }
-                    logger.debug("Send command \"{}\" => {}", cmd.getName(), HexUtils.bytesToHex(message));
-                }
-                break;
-            case ASCII_V1:
-                messageStr = cmd.getAsciiCommandV1();
-                if (messageStr == null) {
-                    logger.debug("Send comman \"{}\" ignored: not available for ASCII V1 protocol", cmd.getName());
-                    return;
-                } else {
-                    if (value != null) {
-                        switch (cmd) {
-                            case VOLUME_SET:
-                                messageStr += String.format("%d", value);
-                                break;
-                            case BASS_SET:
-                            case TREBLE_SET:
-                                if (value == 0) {
-                                    messageStr += "000";
-                                } else if (value > 0) {
-                                    messageStr += String.format("+%02d", value);
-                                } else {
-                                    messageStr += String.format("-%02d", -value);
-                                }
-                                break;
-                            case BALANCE_SET:
-                                if (value == 0) {
-                                    messageStr += "000";
-                                } else if (value > 0) {
-                                    messageStr += String.format("R%02d", value);
-                                } else {
-                                    messageStr += String.format("L%02d", -value);
-                                }
-                                break;
-                            case DIMMER_LEVEL_SET:
-                                if (value > 0 && model.getDimmerLevelMin() < 0) {
-                                    messageStr += String.format("+%d", value);
-                                } else {
-                                    messageStr += String.format("%d", value);
-                                }
-                                break;
-                            default:
-                                break;
-                        }
-                    }
-                    if (!messageStr.endsWith("?")) {
-                        messageStr += "!";
-                    }
-                    message = messageStr.getBytes(StandardCharsets.US_ASCII);
-                    logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
-                }
-                break;
-            case ASCII_V2:
-                messageStr = cmd.getAsciiCommandV2();
-                if (messageStr == null) {
-                    logger.debug("Send comman \"{}\" ignored: not available for ASCII V2 protocol", cmd.getName());
-                    return;
-                } else {
-                    if (value != null) {
-                        switch (cmd) {
-                            case VOLUME_SET:
-                                messageStr += String.format("%02d", value);
-                                break;
-                            case BASS_SET:
-                            case TREBLE_SET:
-                                if (value == 0) {
-                                    messageStr += "000";
-                                } else if (value > 0) {
-                                    messageStr += String.format("+%02d", value);
-                                } else {
-                                    messageStr += String.format("-%02d", -value);
-                                }
-                                break;
-                            case BALANCE_SET:
-                                if (value == 0) {
-                                    messageStr += "000";
-                                } else if (value > 0) {
-                                    messageStr += String.format("r%02d", value);
-                                } else {
-                                    messageStr += String.format("l%02d", -value);
-                                }
-                                break;
-                            case DIMMER_LEVEL_SET:
-                                if (value > 0 && model.getDimmerLevelMin() < 0) {
-                                    messageStr += String.format("+%d", value);
-                                } else {
-                                    messageStr += String.format("%d", value);
-                                }
-                                break;
-                            default:
-                                break;
-                        }
-                    }
-                    if (!messageStr.endsWith("?")) {
-                        messageStr += "!";
-                    }
-                    message = messageStr.getBytes(StandardCharsets.US_ASCII);
-                    logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
-                }
-                break;
-        }
+    public void writeOutput(String cmdName, byte[] dataBuffer) throws RotelException {
         if (simu) {
             return;
         }
         OutputStream dataOut = this.dataOut;
         if (dataOut == null) {
-            throw new RotelException("Send command \"" + cmd.getName() + "\" failed: output stream is null");
+            throw new RotelException("Send command \"" + cmdName + "\" failed: output stream is null");
         }
         try {
-            dataOut.write(message);
+            dataOut.write(dataBuffer);
             dataOut.flush();
         } catch (IOException e) {
-            logger.debug("Send command \"{}\" failed: {}", cmd.getName(), e.getMessage());
-            throw new RotelException("Send command \"" + cmd.getName() + "\" failed", e);
-        }
-        logger.debug("Send command \"{}\" succeeded", cmd.getName());
-    }
-
-    /**
-     * Validate the content of a feedback message
-     *
-     * @param responseMessage the buffer containing the feedback message
-     *
-     * @throws RotelException - If the message has unexpected content
-     */
-    private void validateResponse(byte[] responseMessage) throws RotelException {
-        if (protocol == RotelProtocol.HEX) {
-            // Check minimum message length
-            if (responseMessage.length < 6) {
-                logger.debug("Unexpected message length: {}", responseMessage.length);
-                throw new RotelException("Unexpected message length");
-            }
-
-            // Check START
-            if (responseMessage[0] != START) {
-                logger.debug("Unexpected START in response: {} rather than {}",
-                        Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
-                throw new RotelException("Unexpected START in response");
-            }
-
-            // Check ID
-            if (responseMessage[2] != model.getDeviceId()) {
-                logger.debug("Unexpected ID in response: {} rather than {}",
-                        Integer.toHexString(responseMessage[2] & 0x000000FF),
-                        Integer.toHexString(model.getDeviceId() & 0x000000FF));
-                throw new RotelException("Unexpected ID in response");
-            }
-
-            // Check TYPE
-            if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
-                    && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
-                    && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
-                    && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
-                    && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
-                    && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
-                    && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
-                    && responseMessage[3] != TRIGGER_CMD) {
-                logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
-                throw new RotelException("Unexpected TYPE in response");
-            }
-
-            int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
-                    ? (5 + model.getRespNbChars() + model.getRespNbFlags())
-                    : responseMessage.length;
-
-            // Check COUNT
-            if (responseMessage[1] != (expectedLen - 3)) {
-                logger.debug("Unexpected COUNT in response: {} rather than {}",
-                        Integer.toHexString(responseMessage[1] & 0x000000FF),
-                        Integer.toHexString((expectedLen - 3) & 0x000000FF));
-                throw new RotelException("Unexpected COUNT in response");
-            }
-
-            final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
-            if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
-                expectedLen++;
-            }
-
-            // Check message length
-            if (responseMessage.length != expectedLen) {
-                logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
-                throw new RotelException("Unexpected message length");
-            }
-
-            // Check sum
-            if ((checksum & 0x000000FF) == 0x000000FD) {
-                if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
-                        || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
-                    logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
-                            Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
-                    throw new RotelException("Invalid check sum in response");
-                }
-            } else if ((checksum & 0x000000FF) == 0x000000FE) {
-                if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
-                        || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
-                    logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
-                            Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
-                    throw new RotelException("Invalid check sum in response");
-                }
-            } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
-                logger.debug("Invalid check sum in response: {} rather than {}",
-                        Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
-                        Integer.toHexString(checksum & 0x000000FF));
-                throw new RotelException("Invalid check sum in response");
-            }
-        } else {
-            // Check minimum message length
-            if (responseMessage.length < 1) {
-                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");
-            }
-        }
-    }
-
-    /**
-     * Compute the checksum of a message
-     *
-     * @param message the buffer containing the message
-     * @param maxIdx the position in the buffer at which the sum has to be stopped
-     *
-     * @return the checksum as a byte
-     */
-    protected byte computeCheckSum(byte[] message, int maxIdx) {
-        int result = 0;
-        for (int i = 1; i <= maxIdx; i++) {
-            result += (message[i] & 0x000000FF);
-        }
-        return (byte) (result & 0x000000FF);
-    }
-
-    /**
-     * Add a listener to the list of listeners to be notified with events
-     *
-     * @param listener the listener
-     */
-    public void addEventListener(RotelMessageEventListener listener) {
-        listeners.add(listener);
-    }
-
-    /**
-     * Remove a listener from the list of listeners to be notified with events
-     *
-     * @param listener the listener
-     */
-    public void removeEventListener(RotelMessageEventListener listener) {
-        listeners.remove(listener);
-    }
-
-    /**
-     * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
-     *
-     * @param incomingMessage the received message
-     */
-    public void handleIncomingMessage(byte[] incomingMessage) {
-        logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
-
-        if (READ_ERROR.equals(incomingMessage)) {
-            dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
-            return;
-        }
-
-        try {
-            validateResponse(incomingMessage);
-        } catch (RotelException e) {
-            return;
-        }
-
-        if (protocol == RotelProtocol.HEX) {
-            handleValidHexMessage(incomingMessage);
-        } else {
-            handleValidAsciiMessage(incomingMessage);
-        }
-    }
-
-    /**
-     * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
-     *
-     * @param incomingMessage the received message
-     */
-    private void handleValidHexMessage(byte[] incomingMessage) {
-        if (incomingMessage[3] != STANDARD_RESPONSE) {
-            return;
-        }
-
-        final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
-
-        // Replace characters with code < 32 by a space before converting to a string
-        for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
-            if (incomingMessage[i] < 0x20) {
-                incomingMessage[i] = 0x20;
-            }
-        }
-
-        String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
-        logger.debug("handleValidHexMessage: chars *{}*", value);
-
-        final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
-        final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
-        if (logger.isTraceEnabled()) {
-            for (int i = 1; i <= flags.length; i++) {
-                try {
-                    logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
-                            Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
-                            RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
-                            RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
-                            RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
-                            RotelFlagsMapping.isBitFlagOn(flags, i, 0));
-                } catch (RotelException e1) {
-                }
-            }
-        }
-        try {
-            dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
-        } catch (RotelException e1) {
-        }
-        try {
-            dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
-        } catch (RotelException e1) {
-        }
-        try {
-            dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
-        } catch (RotelException e1) {
-        }
-        boolean checkMultiIn = false;
-        boolean checkSource = true;
-        try {
-            if (model.isMultiInputOn(flags)) {
-                checkSource = false;
-                try {
-                    RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
-                    RotelCommand cmd = source.getCommand();
-                    if (cmd != null) {
-                        String value2 = cmd.getAsciiCommandV2();
-                        if (value2 != null) {
-                            dispatchKeyValue(KEY_SOURCE, value2);
-                        }
-                    }
-                } catch (RotelException e1) {
-                }
-            }
-        } catch (RotelException e1) {
-            checkMultiIn = true;
-        }
-        boolean checkStereo = true;
-        try {
-            checkStereo = !model.isMoreThan2Channels(flags);
-        } catch (RotelException e1) {
-        }
-
-        String valueLowerCase = value.trim().toLowerCase();
-        if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
-                && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
-                && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
-            dispatchKeyValue(KEY_POWER, POWER_ON);
-        }
-
-        if (model.getRespNbChars() == 42) {
-            // 2 lines of 21 characters with a left part and a right part
-
-            // Line 1 left
-            value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
-            logger.debug("handleValidHexMessage: line 1 left *{}*", value);
-            parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
-
-            // Line 1 right
-            value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
-            logger.debug("handleValidHexMessage: line 1 right *{}*", value);
-            parseText(value, false, false, false, false, false, false, false, true);
-
-            // Full line 1
-            value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
-            dispatchKeyValue(KEY_LINE1, value);
-
-            // Line 2 right
-            value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
-            logger.debug("handleValidHexMessage: line 2 right *{}*", value);
-            parseText(value, false, false, false, false, false, false, false, true);
-
-            // Full line 2
-            value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
-            logger.debug("handleValidHexMessage: line 2 *{}*", value);
-            parseText(value, false, false, true, true, false, true, true, true);
-            dispatchKeyValue(KEY_LINE2, value);
-        } else {
-            value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
-            parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
-            dispatchKeyValue(KEY_LINE1, value);
-        }
-
-        if (valueLowerCase.isEmpty()) {
-            dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
-        }
-    }
-
-    /**
-     * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
-     *
-     * @param incomingMessage the received message
-     */
-    public void handleValidAsciiMessage(byte[] incomingMessage) {
-        byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
-
-        // Replace characters with code < 32 by a space before converting to a string
-        for (int i = 0; i < message.length; i++) {
-            if (message[i] < 0x20) {
-                message[i] = 0x20;
-            }
-        }
-
-        String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII);
-        logger.debug("handleValidAsciiMessage: chars *{}*", value);
-        value = value.trim();
-        if (value.isEmpty()) {
-            return;
-        }
-        try {
-            String[] splittedValue = value.split("=");
-            if (splittedValue.length != 2) {
-                logger.debug("handleValidAsciiMessage: ignored message {}", value);
-            } else {
-                dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
-            }
-        } catch (PatternSyntaxException e) {
-            logger.debug("handleValidAsciiMessage: ignored message {}", value);
-        }
-    }
-
-    /**
-     * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
-     *
-     * @param text the text to be parsed
-     * @param searchSource true if a source has to be searched in the text
-     * @param searchMultiIn true if MULTI IN indication has to be searched in the text
-     * @param searchZone true if a zone information has to be searched in the text
-     * @param searchRecord true if a record source has to be searched in the text
-     * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
-     * @param searchDsp true if a DSP mode has to be searched in the text
-     * @param searchStereo true if a STEREO has to be considered in the search
-     * @param multipleInfo true if source and volume/mute are provided separately
-     */
-    private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
-            boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
-            boolean multipleInfo) {
-        String value = text.trim();
-        String valueLowerCase = value.toLowerCase();
-        if (searchRecord) {
-            dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
-        }
-        if (searchZone) {
-            if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
-                dispatchKeyValue(KEY_ZONE, "2");
-            } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
-                dispatchKeyValue(KEY_ZONE, "3");
-            } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
-                dispatchKeyValue(KEY_ZONE, "4");
-            } else {
-                dispatchKeyValue(KEY_ZONE, "1");
-            }
-        }
-        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
-            value = extractNumber(value,
-                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
-            dispatchKeyValue(KEY_VOLUME, value);
-            dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
-        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
-            value = value.substring(KEY_HEX_MUTE.length()).trim();
-            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
-            } else {
-                logger.debug("Invalid value {} for zone mute", value);
-            }
-        } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
-            value = extractNumber(value,
-                    valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
-            dispatchKeyValue(KEY_BASS, value);
-        } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
-            value = extractNumber(value,
-                    valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
-            dispatchKeyValue(KEY_TREBLE, value);
-        } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
-            value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
-            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                try {
-                    RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
-                    RotelCommand cmd = source.getCommand();
-                    if (cmd != null) {
-                        String value2 = cmd.getAsciiCommandV2();
-                        if (value2 != null) {
-                            dispatchKeyValue(KEY_SOURCE, value2);
-                        }
-                    }
-                } catch (RotelException e1) {
-                }
-            } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-                logger.debug("Invalid value {} for MULTI IN", value);
-            }
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_BYPASS.getFeedback());
-        } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
-        } else if (searchDsp
-                && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
-        } else if (searchDsp
-                && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
-        } else if (searchDsp
-                && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
-        } else if (searchDsp
-                && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
-        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
-                || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
-                || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
-        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
-                || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
-                || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
-        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
-                || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
-                || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
-        } else if (searchDsp
-                && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
-        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
-                || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
-        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
-                || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
-            logger.debug("DTS-ES");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
-            logger.debug("DTS 96");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
-            logger.debug("DTS");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
-            logger.debug("DD-EX");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
-            logger.debug("DD");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
-            logger.debug("LPCM");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
-            logger.debug("PCM");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
-            logger.debug("MPEG");
-            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
-        } else if (searchZone
-                && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
-            value = value.substring(
-                    valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
-            parseZone2(value, multipleInfo);
-        } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
-            parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
-        } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
-            parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
-        } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
-            parseRecord(value.substring(KEY_HEX_RECORD.length()));
-        } else if (searchSource || searchRecordAfterSource) {
-            parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
-        }
-    }
-
-    /**
-     * Parse a text to identify a source
-     *
-     * @param text the text to be parsed
-     * @param acceptFollowMain true if follow main has to be considered in the search
-     *
-     * @return the identified source or null if no source is identified in the text
-     */
-    private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
-        String value = text.trim();
-        RotelSource source = null;
-        if (!value.isEmpty()) {
-            if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
-                try {
-                    source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
-                } catch (RotelException e) {
-                }
-            } else {
-                for (RotelSource src : sourcesLabels.keySet()) {
-                    String label = sourcesLabels.get(src);
-                    if (label != null && value.startsWith(label)) {
-                        if (source == null || sourcesLabels.get(source).length() < label.length()) {
-                            source = src;
-                        }
-                    }
-                }
-            }
-        }
-        return source;
-    }
-
-    private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
-            boolean multipleInfo) {
-        RotelSource source = parseSource(text, false);
-        if (source != null) {
-            if (searchSource) {
-                RotelCommand cmd = source.getCommand();
-                if (cmd != null) {
-                    String value2 = cmd.getAsciiCommandV2();
-                    if (value2 != null) {
-                        dispatchKeyValue(KEY_SOURCE, value2);
-                        if (!multipleInfo) {
-                            dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
-                        }
-                    }
-                }
-            }
-
-            if (searchRecordAfterSource) {
-                String value = text.substring(getSourceLabel(source).length()).trim();
-                source = parseSource(value, true);
-                if (source != null) {
-                    RotelCommand cmd = source.getRecordCommand();
-                    if (cmd != null) {
-                        value = cmd.getAsciiCommandV2();
-                        if (value != null) {
-                            dispatchKeyValue(KEY_RECORD, value);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    private String getSourceLabel(RotelSource source) {
-        String label = sourcesLabels.get(source);
-        return (label == null) ? source.getLabel() : label;
-    }
-
-    private void parseRecord(String text) {
-        String value = text.trim();
-        RotelSource source = parseSource(value, true);
-        if (source != null) {
-            RotelCommand cmd = source.getRecordCommand();
-            if (cmd != null) {
-                value = cmd.getAsciiCommandV2();
-                if (value != null) {
-                    dispatchKeyValue(KEY_RECORD, value);
-                }
-            }
-        } else {
-            logger.debug("Invalid value {} for record source", value);
-        }
-    }
-
-    private void parseZone2(String text, boolean multipleInfo) {
-        String value = text.trim();
-        String valueLowerCase = value.toLowerCase();
-        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
-            value = extractNumber(value,
-                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
-            dispatchKeyValue(KEY_VOLUME_ZONE2, value);
-            dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
-        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
-            value = value.substring(KEY_HEX_MUTE.length()).trim();
-            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
-            } else {
-                logger.debug("Invalid value {} for zone mute", value);
-            }
-        } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-            RotelSource source = parseSource(value, true);
-            if (source != null) {
-                RotelCommand cmd = source.getZone2Command();
-                if (cmd != null) {
-                    value = cmd.getAsciiCommandV2();
-                    if (value != null) {
-                        dispatchKeyValue(KEY_SOURCE_ZONE2, value);
-                        if (!multipleInfo) {
-                            dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
-                        }
-                    }
-                }
-            } else {
-                logger.debug("Invalid value {} for zone 2 source", value);
-            }
-        }
-    }
-
-    private void parseZone3(String text, boolean multipleInfo) {
-        String value = text.trim();
-        String valueLowerCase = value.toLowerCase();
-        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
-            value = extractNumber(value,
-                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
-            dispatchKeyValue(KEY_VOLUME_ZONE3, value);
-            dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
-        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
-            value = value.substring(KEY_HEX_MUTE.length()).trim();
-            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
-            } else {
-                logger.debug("Invalid value {} for zone mute", value);
-            }
-        } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-            RotelSource source = parseSource(value, true);
-            if (source != null) {
-                RotelCommand cmd = source.getZone3Command();
-                if (cmd != null) {
-                    value = cmd.getAsciiCommandV2();
-                    if (value != null) {
-                        dispatchKeyValue(KEY_SOURCE_ZONE3, value);
-                        if (!multipleInfo) {
-                            dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
-                        }
-                    }
-                }
-            } else {
-                logger.debug("Invalid value {} for zone 3 source", value);
-            }
-        }
-    }
-
-    private void parseZone4(String text, boolean multipleInfo) {
-        String value = text.trim();
-        String valueLowerCase = value.toLowerCase();
-        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
-            value = extractNumber(value,
-                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
-            dispatchKeyValue(KEY_VOLUME_ZONE4, value);
-            dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
-        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
-            value = value.substring(KEY_HEX_MUTE.length()).trim();
-            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
-            } else {
-                logger.debug("Invalid value {} for zone mute", value);
-            }
-        } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-            RotelSource source = parseSource(value, true);
-            if (source != null) {
-                RotelCommand cmd = source.getZone4Command();
-                if (cmd != null) {
-                    value = cmd.getAsciiCommandV2();
-                    if (value != null) {
-                        dispatchKeyValue(KEY_SOURCE_ZONE4, value);
-                        if (!multipleInfo) {
-                            dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
-                        }
-                    }
-                }
-            } else {
-                logger.debug("Invalid value {} for zone 4 source", value);
-            }
-        }
-    }
-
-    /**
-     * Extract from a string a number
-     *
-     * @param value the string
-     * @param startIndex the index in the string at which the integer has to be extracted
-     *
-     * @return the number as a string with its sign and no blank between the sign and the digits
-     */
-    private String extractNumber(String value, int startIndex) {
-        String result = value.substring(startIndex).trim();
-        // Delete possible blank(s) between the sign and the number
-        if (result.startsWith("+") || result.startsWith("-")) {
-            result = result.substring(0, 1) + result.substring(1, result.length()).trim();
-        }
-        return result;
-    }
-
-    /**
-     * Suppress certain sequences of bytes from a message
-     *
-     * @param message the message as a table of bytes
-     * @param bytesSequences the table containing the sequence of bytes to be ignored
-     *
-     * @return the message without the unexpected sequence of bytes
-     */
-    private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
-        if (bytesSequences.length == 0) {
-            return message;
-        }
-        byte[] filteredMsg = new byte[message.length];
-        int srcIdx = 0;
-        int dstIdx = 0;
-        while (srcIdx < message.length) {
-            int ignoredLength = 0;
-            for (int i = 0; i < bytesSequences.length; i++) {
-                int size = bytesSequences[i].length;
-                if ((message.length - srcIdx) >= size) {
-                    boolean match = true;
-                    for (int j = 0; j < size; j++) {
-                        if (message[srcIdx + j] != bytesSequences[i][j]) {
-                            match = false;
-                            break;
-                        }
-                    }
-                    if (match) {
-                        ignoredLength = size;
-                        break;
-                    }
-                }
-            }
-            if (ignoredLength > 0) {
-                srcIdx += ignoredLength;
-            } else {
-                filteredMsg[dstIdx++] = message[srcIdx++];
-            }
-        }
-        return Arrays.copyOf(filteredMsg, dstIdx);
-    }
-
-    /**
-     * Dispatch an event (key, value) to the event listeners
-     *
-     * @param key the key
-     * @param value the value
-     */
-    private void dispatchKeyValue(String key, String value) {
-        RotelMessageEvent event = new RotelMessageEvent(this, key, value);
-        for (int i = 0; i < listeners.size(); i++) {
-            listeners.get(i).onNewMessageEvent(event);
+            logger.debug("Send command \"{}\" failed: {}", cmdName, e.getMessage());
+            throw new RotelException("Send command \"" + cmdName + "\" failed", e);
         }
+        logger.debug("Send command \"{}\" succeeded", cmdName);
     }
 }
index 6ca28af2d8314a83c1ea816a66f1939c1db7c095..2d53d0326b2791f110405e226a9582e16cc7c840 100644 (file)
@@ -19,12 +19,11 @@ import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.net.Socket;
 import java.net.SocketTimeoutException;
-import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.rotel.internal.RotelException;
-import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,13 +47,12 @@ public class RotelIpConnector extends RotelConnector {
      *
      * @param address the IP address of the projector
      * @param port the TCP port to be used
-     * @param model the projector model in use
-     * @param protocol the protocol to be used
+     * @param protocolHandler the protocol handler
      * @param readerThreadName the name of thread to be created
      */
-    public RotelIpConnector(String address, Integer port, RotelModel model, RotelProtocol protocol,
-            Map<RotelSource, String> sourcesLabels, String readerThreadName) {
-        super(model, protocol, sourcesLabels, false, readerThreadName);
+    public RotelIpConnector(String address, Integer port, RotelAbstractProtocolHandler protocolHandler,
+            String readerThreadName) {
+        super(protocolHandler, false, readerThreadName);
 
         this.address = address;
         this.port = port;
@@ -70,9 +68,7 @@ public class RotelIpConnector extends RotelConnector {
             dataOut = new DataOutputStream(clientSocket.getOutputStream());
             dataIn = new DataInputStream(clientSocket.getInputStream());
 
-            Thread thread = new RotelReaderThread(this, readerThreadName);
-            setReaderThread(thread);
-            thread.start();
+            readerThread.start();
 
             this.clientSocket = clientSocket;
 
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelMessageEvent.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelMessageEvent.java
deleted file mode 100644 (file)
index 6ba460d..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.rotel.internal.communication;
-
-import java.util.EventObject;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Rotel event used to notify changes coming from messages received from the Rotel device
- *
- * @author Laurent Garnier - Initial contribution
- */
-@NonNullByDefault
-public class RotelMessageEvent extends EventObject {
-
-    private static final long serialVersionUID = 1L;
-    private String key;
-    private String value;
-
-    public RotelMessageEvent(Object source, String key, String value) {
-        super(source);
-        this.key = key;
-        this.value = value;
-    }
-
-    public String getKey() {
-        return key;
-    }
-
-    public String getValue() {
-        return value;
-    }
-}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelMessageEventListener.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelMessageEventListener.java
deleted file mode 100644 (file)
index c710ae0..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.rotel.internal.communication;
-
-import java.util.EventListener;
-import java.util.EventObject;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Rotel Event Listener interface. Handles incoming Rotel message events
- *
- * @author Laurent Garnier - Initial contribution
- */
-@NonNullByDefault
-public interface RotelMessageEventListener extends EventListener {
-
-    /**
-     * Event handler method for incoming Rotel message events
-     *
-     * @param event the event object
-     */
-    public void onNewMessageEvent(EventObject event);
-}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelProtocol.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelProtocol.java
deleted file mode 100644 (file)
index 07c53d1..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.rotel.internal.communication;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.rotel.internal.RotelException;
-
-/**
- * Represents the different kinds of protocols
- *
- * @author Laurent Garnier - Initial contribution
- */
-@NonNullByDefault
-public enum RotelProtocol {
-
-    HEX("HEX"),
-    ASCII_V1("ASCII_V1"),
-    ASCII_V2("ASCII_V2");
-
-    private String name;
-
-    /**
-     * Constructor
-     *
-     * @param name the protocol name
-     */
-    private RotelProtocol(String name) {
-        this.name = name;
-    }
-
-    /**
-     * Get the protocol name
-     *
-     * @return the protocol name
-     */
-    public String getName() {
-        return name;
-    }
-
-    /**
-     * Get the protocol associated to a name
-     *
-     * @param name the name used to identify the protocol
-     *
-     * @return the protocol associated to the searched name
-     *
-     * @throws RotelException - If no protocol is associated to the searched name
-     */
-    public static RotelProtocol getFromName(String name) throws RotelException {
-        for (RotelProtocol value : RotelProtocol.values()) {
-            if (value.getName().equals(name)) {
-                return value;
-            }
-        }
-        throw new RotelException("Invalid protocol name: " + name);
-    }
-}
index e629c39949473f16e1248b2dd98192e4dc98d180..52f5efab526d896a7a7185c08193f3af2708d312 100644 (file)
 package org.openhab.binding.rotel.internal.communication;
 
 import java.io.InterruptedIOException;
-import java.util.Arrays;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.rotel.internal.RotelException;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -28,79 +28,38 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 public class RotelReaderThread extends Thread {
 
-    private final Logger logger = LoggerFactory.getLogger(RotelReaderThread.class);
-
     private static final int READ_BUFFER_SIZE = 16;
 
-    private RotelConnector connector;
+    private final Logger logger = LoggerFactory.getLogger(RotelReaderThread.class);
+
+    private final RotelConnector connector;
+    private final RotelAbstractProtocolHandler protocolHandler;
 
     /**
      * Constructor
      *
-     * @param connector the object that should handle the received message
+     * @param connector the connector to read input data
+     * @param protocolHandler the protocol handler
      * @param threadName the name of the thread
      */
-    public RotelReaderThread(RotelConnector connector, String threadName) {
+    public RotelReaderThread(RotelConnector connector, RotelAbstractProtocolHandler protocolHandler,
+            String threadName) {
         super(threadName);
         this.connector = connector;
+        this.protocolHandler = protocolHandler;
     }
 
     @Override
     public void run() {
         logger.debug("Data listener started");
 
-        RotelProtocol protocol = connector.getProtocol();
-        final int size = (protocol == RotelProtocol.HEX)
-                ? (6 + connector.getModel().getRespNbChars() + connector.getModel().getRespNbFlags())
-                : 64;
         byte[] readDataBuffer = new byte[READ_BUFFER_SIZE];
-        byte[] dataBuffer = new byte[size];
-        boolean startCodeReached = false;
-        int count = 0;
-        int index = 0;
-        final char terminatingChar = (protocol == RotelProtocol.ASCII_V1) ? '!' : '$';
 
         try {
             while (!Thread.interrupted()) {
                 int len = connector.readInput(readDataBuffer);
                 if (len > 0) {
-                    for (int i = 0; i < len; i++) {
-                        if (protocol == RotelProtocol.HEX) {
-                            if (readDataBuffer[i] == RotelConnector.START) {
-                                startCodeReached = true;
-                                count = 0;
-                                index = 0;
-                            }
-                            if (startCodeReached) {
-                                if (index < size) {
-                                    dataBuffer[index++] = readDataBuffer[i];
-                                }
-                                if (index == 2) {
-                                    count = readDataBuffer[i];
-                                } else if ((count > 0) && (index == (count + 3))) {
-                                    if ((readDataBuffer[i] & 0x000000FF) == 0x000000FD) {
-                                        count++;
-                                    } else {
-                                        byte[] msg = Arrays.copyOf(dataBuffer, index);
-                                        connector.handleIncomingMessage(msg);
-                                        startCodeReached = false;
-                                    }
-                                }
-                            }
-                        } else {
-                            if (index < size) {
-                                dataBuffer[index++] = readDataBuffer[i];
-                            }
-                            if (readDataBuffer[i] == terminatingChar) {
-                                if (index >= size) {
-                                    dataBuffer[index - 1] = (byte) terminatingChar;
-                                }
-                                byte[] msg = Arrays.copyOf(dataBuffer, index);
-                                connector.handleIncomingMessage(msg);
-                                index = 0;
-                            }
-                        }
-                    }
+                    protocolHandler.handleIncomingData(readDataBuffer, len);
                 }
             }
         } catch (InterruptedIOException e) {
@@ -108,7 +67,7 @@ public class RotelReaderThread extends Thread {
             logger.debug("Interrupted via InterruptedIOException");
         } catch (RotelException e) {
             logger.debug("Reading failed: {}", e.getMessage(), e);
-            connector.handleIncomingMessage(RotelConnector.READ_ERROR);
+            protocolHandler.handleInIncomingError();
         }
 
         logger.debug("Data listener stopped");
index bf9114bb06c39520605a775a94f47935ed9a9ac9..a1abeb8f1e17e6d3c119552e1db4de2a9c82824e 100644 (file)
@@ -15,12 +15,11 @@ package org.openhab.binding.rotel.internal.communication;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.rotel.internal.RotelException;
-import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
 import org.openhab.core.io.transport.serial.PortInUseException;
 import org.openhab.core.io.transport.serial.SerialPort;
 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
@@ -42,6 +41,8 @@ public class RotelSerialConnector extends RotelConnector {
     private String serialPortName;
     private SerialPortManager serialPortManager;
 
+    private int baudRate;
+
     private @Nullable SerialPort serialPort;
 
     /**
@@ -49,14 +50,15 @@ public class RotelSerialConnector extends RotelConnector {
      *
      * @param serialPortManager the serial port manager
      * @param serialPortName the serial port name to be used
-     * @param model the projector model in use
-     * @param protocol the protocol to be used
+     * @param baudRate the baud rate to be used
+     * @param protocolHandler the protocol handler
      * @param readerThreadName the name of thread to be created
      */
-    public RotelSerialConnector(SerialPortManager serialPortManager, String serialPortName, RotelModel model,
-            RotelProtocol protocol, Map<RotelSource, String> sourcesLabels, String readerThreadName) {
-        super(model, protocol, sourcesLabels, false, readerThreadName);
+    public RotelSerialConnector(SerialPortManager serialPortManager, String serialPortName, int baudRate,
+            RotelAbstractProtocolHandler protocolHandler, String readerThreadName) {
+        super(protocolHandler, false, readerThreadName);
 
+        this.baudRate = baudRate;
         this.serialPortManager = serialPortManager;
         this.serialPortName = serialPortName;
     }
@@ -73,7 +75,7 @@ public class RotelSerialConnector extends RotelConnector {
 
             SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
 
-            commPort.setSerialPortParams(getModel().getBaudRate(), SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
+            commPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
                     SerialPort.PARITY_NONE);
             commPort.enableReceiveThreshold(1);
             commPort.enableReceiveTimeout(100);
@@ -92,9 +94,7 @@ public class RotelSerialConnector extends RotelConnector {
                 }
             }
 
-            Thread thread = new RotelReaderThread(this, readerThreadName);
-            setReaderThread(thread);
-            thread.start();
+            readerThread.start();
 
             this.serialPort = commPort;
             this.dataIn = dataIn;
index 4f3e543f58ba6602173d3a574def1b03ca5defbe..2062aceed082e0b034c360cd0efb2a1c2de525fc 100644 (file)
@@ -12,6 +12,9 @@
  */
 package org.openhab.binding.rotel.internal.communication;
 
+import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
+import static org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler.START;
+
 import java.io.InterruptedIOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -23,6 +26,9 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.rotel.internal.RotelException;
 import org.openhab.binding.rotel.internal.RotelModel;
 import org.openhab.binding.rotel.internal.RotelPlayStatus;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
+import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
+import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -34,9 +40,13 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 public class RotelSimuConnector extends RotelConnector {
 
+    private static final int STEP_TONE_LEVEL = 1;
+
     private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
 
-    private static final int STEP_TONE_LEVEL = 1;
+    private final RotelModel model;
+    private final RotelProtocol protocol;
+    private final Map<RotelSource, String> sourcesLabels;
 
     private Object lock = new Object();
 
@@ -80,12 +90,16 @@ public class RotelSimuConnector extends RotelConnector {
      * Constructor
      *
      * @param model the projector model in use
-     * @param protocol the protocol to be used
+     * @param protocolHandler the protocol handler
+     * @param sourcesLabels the custom labels for sources
      * @param readerThreadName the name of thread to be created
      */
-    public RotelSimuConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
-            String readerThreadName) {
-        super(model, protocol, sourcesLabels, true, readerThreadName);
+    public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
+            Map<RotelSource, String> sourcesLabels, String readerThreadName) {
+        super(protocolHandler, true, readerThreadName);
+        this.model = model;
+        this.protocol = protocolHandler.getProtocol();
+        this.sourcesLabels = sourcesLabels;
         this.minVolume = 0;
         this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
         this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
@@ -95,9 +109,7 @@ public class RotelSimuConnector extends RotelConnector {
     @Override
     public synchronized void open() throws RotelException {
         logger.debug("Opening simulated connection");
-        Thread thread = new RotelReaderThread(this, readerThreadName);
-        setReaderThread(thread);
-        thread.start();
+        readerThread.start();
         setConnected(true);
         logger.debug("Simulated connection opened");
     }
@@ -132,23 +144,13 @@ public class RotelSimuConnector extends RotelConnector {
         return 0;
     }
 
-    @Override
-    public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
-        super.sendCommand(cmd, value);
-        if ((getProtocol() == RotelProtocol.HEX && cmd.getHexType() != 0)
-                || (getProtocol() == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
-                || (getProtocol() == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
-            buildFeedbackMessage(cmd, value);
-        }
-    }
-
     /**
      * Built the simulated feedback message for a sent command
      *
      * @param cmd the sent command
      * @param value the integer value considered in the sent command for volume, bass or treble adjustment
      */
-    private void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
+    public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
         String text = buildSourceLine1Response();
         String textLine1Left = buildSourceLine1LeftResponse();
         String textLine1Right = buildVolumeLine1RightResponse();
@@ -180,14 +182,14 @@ public class RotelSimuConnector extends RotelConnector {
                 break;
             case ZONE2_POWER_OFF:
                 powerZone2 = false;
-                text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                         powerZone2, sourceZone2);
                 showZone = 2;
                 resetZone = false;
                 break;
             case ZONE2_POWER_ON:
                 powerZone2 = true;
-                text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                         powerZone2, sourceZone2);
                 showZone = 2;
                 resetZone = false;
@@ -217,9 +219,9 @@ public class RotelSimuConnector extends RotelConnector {
                 resetZone = false;
                 break;
             case RECORD_FONCTION_SELECT:
-                if (getModel().getNbAdditionalZones() >= 1 && getModel().getZoneSelectCmd() == cmd) {
+                if (model.getNbAdditionalZones() >= 1 && model.getZoneSelectCmd() == cmd) {
                     showZone++;
-                    if (showZone > getModel().getNbAdditionalZones()) {
+                    if (showZone > model.getNbAdditionalZones()) {
                         showZone = 1;
                         if (!power) {
                             showZone++;
@@ -234,7 +236,7 @@ public class RotelSimuConnector extends RotelConnector {
                     textLine2 = buildRecordResponse();
                 } else if (showZone == 2) {
                     selectingRecord = false;
-                    text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             powerZone2, sourceZone2);
                 } else if (showZone == 3) {
                     selectingRecord = false;
@@ -246,12 +248,12 @@ public class RotelSimuConnector extends RotelConnector {
                 resetZone = false;
                 break;
             case ZONE_SELECT:
-                if (getModel().getNbAdditionalZones() == 0
-                        || (getModel().getNbAdditionalZones() > 1 && getModel().getZoneSelectCmd() == cmd)
-                        || (showZone == 1 && getModel().getZoneSelectCmd() != cmd)) {
+                if (model.getNbAdditionalZones() == 0
+                        || (model.getNbAdditionalZones() > 1 && model.getZoneSelectCmd() == cmd)
+                        || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
                     accepted = false;
                 } else {
-                    if (getModel().getZoneSelectCmd() == cmd) {
+                    if (model.getZoneSelectCmd() == cmd) {
                         if (!power && !powerZone2) {
                             showZone = 2;
                             powerZone2 = true;
@@ -270,8 +272,8 @@ public class RotelSimuConnector extends RotelConnector {
                         }
                     }
                     if (showZone == 2) {
-                        text = textLine2 = buildZonePowerResponse(
-                                getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", powerZone2, sourceZone2);
+                        text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                                powerZone2, sourceZone2);
                     } else if (showZone == 3) {
                         text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
                     } else if (showZone == 4) {
@@ -291,54 +293,54 @@ public class RotelSimuConnector extends RotelConnector {
                     if (volumeZone2 < maxVolume) {
                         volumeZone2++;
                     }
-                    text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             muteZone2, volumeZone2);
                     break;
                 case ZONE2_VOLUME_DOWN:
                     if (volumeZone2 > minVolume) {
                         volumeZone2--;
                     }
-                    text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             muteZone2, volumeZone2);
                     break;
                 case ZONE2_VOLUME_SET:
                     if (value != null) {
                         volumeZone2 = value;
                     }
-                    text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             muteZone2, volumeZone2);
                     break;
                 case VOLUME_UP:
-                    if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
+                    if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
                         if (volumeZone2 < maxVolume) {
                             volumeZone2++;
                         }
-                        text = textLine2 = buildZoneVolumeResponse(
-                                getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
+                        text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                                muteZone2, volumeZone2);
                         resetZone = false;
                     } else {
                         accepted = false;
                     }
                     break;
                 case VOLUME_DOWN:
-                    if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
+                    if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
                         if (volumeZone2 > minVolume) {
                             volumeZone2--;
                         }
-                        text = textLine2 = buildZoneVolumeResponse(
-                                getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
+                        text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                                muteZone2, volumeZone2);
                         resetZone = false;
                     } else {
                         accepted = false;
                     }
                     break;
                 case VOLUME_SET:
-                    if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
+                    if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
                         if (value != null) {
                             volumeZone2 = value;
                         }
-                        text = textLine2 = buildZoneVolumeResponse(
-                                getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
+                        text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                                muteZone2, volumeZone2);
                         resetZone = false;
                     } else {
                         accepted = false;
@@ -346,17 +348,17 @@ public class RotelSimuConnector extends RotelConnector {
                     break;
                 case ZONE2_MUTE_TOGGLE:
                     muteZone2 = !muteZone2;
-                    text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             muteZone2, volumeZone2);
                     break;
                 case ZONE2_MUTE_ON:
                     muteZone2 = true;
-                    text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             muteZone2, volumeZone2);
                     break;
                 case ZONE2_MUTE_OFF:
                     muteZone2 = false;
-                    text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             muteZone2, volumeZone2);
                     break;
                 default:
@@ -365,9 +367,9 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    sourceZone2 = getModel().getZone2SourceFromCommand(cmd);
+                    sourceZone2 = model.getZone2SourceFromCommand(cmd);
                     powerZone2 = true;
-                    text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             powerZone2, sourceZone2);
                     muteZone2 = false;
                     accepted = true;
@@ -376,12 +378,11 @@ public class RotelSimuConnector extends RotelConnector {
                 } catch (RotelException e) {
                 }
             }
-            if (!accepted && !getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1
-                    && showZone == 2) {
+            if (!accepted && !model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
                 try {
-                    sourceZone2 = getModel().getSourceFromCommand(cmd);
+                    sourceZone2 = model.getSourceFromCommand(cmd);
                     powerZone2 = true;
-                    text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
+                    text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
                             powerZone2, sourceZone2);
                     muteZone2 = false;
                     accepted = true;
@@ -429,7 +430,7 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    sourceZone3 = getModel().getZone3SourceFromCommand(cmd);
+                    sourceZone3 = model.getZone3SourceFromCommand(cmd);
                     powerZone3 = true;
                     text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
                     muteZone3 = false;
@@ -479,7 +480,7 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    sourceZone4 = getModel().getZone4SourceFromCommand(cmd);
+                    sourceZone4 = model.getZone4SourceFromCommand(cmd);
                     powerZone4 = true;
                     text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
                     muteZone4 = false;
@@ -495,11 +496,11 @@ public class RotelSimuConnector extends RotelConnector {
             switch (cmd) {
                 case UPDATE_AUTO:
                     textAscii = buildAsciiResponse(
-                            getProtocol() == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, "AUTO");
+                            protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
                     break;
                 case UPDATE_MANUAL:
                     textAscii = buildAsciiResponse(
-                            getProtocol() == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, "MANUAL");
+                            protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
                     break;
                 case VOLUME_GET_MIN:
                     textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
@@ -668,7 +669,7 @@ public class RotelSimuConnector extends RotelConnector {
                     multiinput = !multiinput;
                     text = "MULTI IN " + (multiinput ? "ON" : "OFF");
                     try {
-                        source = getModel().getSourceFromCommand(cmd);
+                        source = model.getSourceFromCommand(cmd);
                         textLine1Left = buildSourceLine1LeftResponse();
                         textAscii = buildSourceAsciiResponse();
                         mute = false;
@@ -795,7 +796,7 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    source = getModel().getMainZoneSourceFromCommand(cmd);
+                    source = model.getMainZoneSourceFromCommand(cmd);
                     text = buildSourceLine1Response();
                     textLine1Left = buildSourceLine1LeftResponse();
                     textAscii = buildSourceAsciiResponse();
@@ -805,10 +806,10 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    if (selectingRecord && !getModel().hasOtherThanPrimaryCommands()) {
-                        recordSource = getModel().getSourceFromCommand(cmd);
+                    if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
+                        recordSource = model.getSourceFromCommand(cmd);
                     } else {
-                        source = getModel().getSourceFromCommand(cmd);
+                        source = model.getSourceFromCommand(cmd);
                     }
                     text = buildSourceLine1Response();
                     textLine1Left = buildSourceLine1LeftResponse();
@@ -820,7 +821,7 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    recordSource = getModel().getRecordSourceFromCommand(cmd);
+                    recordSource = model.getRecordSourceFromCommand(cmd);
                     text = buildSourceLine1Response();
                     textLine2 = buildRecordResponse();
                     accepted = true;
@@ -840,7 +841,7 @@ public class RotelSimuConnector extends RotelConnector {
             showZone = 0;
         }
 
-        if (getModel().getRespNbChars() == 42) {
+        if (model.getRespNbChars() == 42) {
             while (textLine1Left.length() < 14) {
                 textLine1Left += " ";
             }
@@ -853,44 +854,44 @@ public class RotelSimuConnector extends RotelConnector {
             text = textLine1Left + textLine1Right + textLine2;
         }
 
-        if (getProtocol() == RotelProtocol.HEX) {
-            byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), getModel().getRespNbChars());
-            byte[] flags = new byte[getModel().getRespNbFlags()];
+        if (protocol == RotelProtocol.HEX) {
+            byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
+            byte[] flags = new byte[model.getRespNbFlags()];
             try {
-                getModel().setMultiInput(flags, multiinput);
+                model.setMultiInput(flags, multiinput);
             } catch (RotelException e) {
             }
             try {
-                getModel().setZone2(flags, powerZone2);
+                model.setZone2(flags, powerZone2);
             } catch (RotelException e) {
             }
             try {
-                getModel().setZone3(flags, powerZone3);
+                model.setZone3(flags, powerZone3);
             } catch (RotelException e) {
             }
             try {
-                getModel().setZone4(flags, powerZone4);
+                model.setZone4(flags, powerZone4);
             } catch (RotelException e) {
             }
-            int size = 6 + getModel().getRespNbChars() + getModel().getRespNbFlags();
+            int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
             byte[] dataBuffer = new byte[size];
             int idx = 0;
             dataBuffer[idx++] = START;
             dataBuffer[idx++] = (byte) (size - 4);
-            dataBuffer[idx++] = getModel().getDeviceId();
+            dataBuffer[idx++] = model.getDeviceId();
             dataBuffer[idx++] = STANDARD_RESPONSE;
-            if (getModel().isCharsBeforeFlags()) {
-                System.arraycopy(chars, 0, dataBuffer, idx, getModel().getRespNbChars());
-                idx += getModel().getRespNbChars();
-                System.arraycopy(flags, 0, dataBuffer, idx, getModel().getRespNbFlags());
-                idx += getModel().getRespNbFlags();
+            if (model.isCharsBeforeFlags()) {
+                System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
+                idx += model.getRespNbChars();
+                System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
+                idx += model.getRespNbFlags();
             } else {
-                System.arraycopy(flags, 0, dataBuffer, idx, getModel().getRespNbFlags());
-                idx += getModel().getRespNbFlags();
-                System.arraycopy(chars, 0, dataBuffer, idx, getModel().getRespNbChars());
-                idx += getModel().getRespNbChars();
+                System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
+                idx += model.getRespNbFlags();
+                System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
+                idx += model.getRespNbChars();
             }
-            byte checksum = computeCheckSum(dataBuffer, idx - 1);
+            byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
             if ((checksum & 0x000000FF) == 0x000000FD) {
                 dataBuffer[idx++] = (byte) 0xFD;
                 dataBuffer[idx++] = 0;
@@ -905,7 +906,7 @@ public class RotelSimuConnector extends RotelConnector {
                 idxInFeedbackMsg = 0;
             }
         } else {
-            String command = textAscii + (getProtocol() == RotelProtocol.ASCII_V1 ? "!" : "$");
+            String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
             synchronized (lock) {
                 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
                 idxInFeedbackMsg = 0;
@@ -978,8 +979,7 @@ public class RotelSimuConnector extends RotelConnector {
                 status = STOP;
                 break;
         }
-        return buildAsciiResponse(getProtocol() == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS,
-                status);
+        return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
     }
 
     private String buildTrackAsciiResponse() {
index bb64cb328382d10750f130f86d8207b86c7e01a3..4a383e7f0ef754478cee50a380f1688371e6f9ab 100644 (file)
@@ -34,13 +34,17 @@ import org.openhab.binding.rotel.internal.communication.RotelCommand;
 import org.openhab.binding.rotel.internal.communication.RotelConnector;
 import org.openhab.binding.rotel.internal.communication.RotelDsp;
 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
-import org.openhab.binding.rotel.internal.communication.RotelMessageEvent;
-import org.openhab.binding.rotel.internal.communication.RotelMessageEventListener;
-import org.openhab.binding.rotel.internal.communication.RotelProtocol;
 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
 import org.openhab.binding.rotel.internal.communication.RotelSource;
 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
+import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
+import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
+import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
+import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
+import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
+import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
 import org.openhab.core.io.transport.serial.SerialPortManager;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.IncreaseDecreaseType;
@@ -87,8 +91,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     private RotelStateDescriptionOptionProvider stateDescriptionProvider;
     private SerialPortManager serialPortManager;
 
-    private RotelConnector connector = new RotelSimuConnector(DEFAULT_MODEL, RotelProtocol.HEX, new HashMap<>(),
-            "OH-binding-rotel");
+    private RotelModel model;
+    private RotelProtocol protocol;
+    private RotelAbstractProtocolHandler protocolHandler;
+    private RotelConnector connector;
 
     private int minVolume;
     private int maxVolume;
@@ -143,193 +149,196 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
         super(thing);
         this.stateDescriptionProvider = stateDescriptionProvider;
         this.serialPortManager = serialPortManager;
+        this.model = DEFAULT_MODEL;
+        this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
+        this.protocol = protocolHandler.getProtocol();
+        this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
     }
 
     @Override
     public void initialize() {
         logger.debug("Start initializing handler for thing {}", getThing().getUID());
 
-        RotelModel rotelModel;
         switch (getThing().getThingTypeUID().getId()) {
             case THING_TYPE_ID_RSP1066:
-                rotelModel = RotelModel.RSP1066;
+                model = RotelModel.RSP1066;
                 break;
             case THING_TYPE_ID_RSP1068:
-                rotelModel = RotelModel.RSP1068;
+                model = RotelModel.RSP1068;
                 break;
             case THING_TYPE_ID_RSP1069:
-                rotelModel = RotelModel.RSP1069;
+                model = RotelModel.RSP1069;
                 break;
             case THING_TYPE_ID_RSP1098:
-                rotelModel = RotelModel.RSP1098;
+                model = RotelModel.RSP1098;
                 break;
             case THING_TYPE_ID_RSP1570:
-                rotelModel = RotelModel.RSP1570;
+                model = RotelModel.RSP1570;
                 break;
             case THING_TYPE_ID_RSP1572:
-                rotelModel = RotelModel.RSP1572;
+                model = RotelModel.RSP1572;
                 break;
             case THING_TYPE_ID_RSX1055:
-                rotelModel = RotelModel.RSX1055;
+                model = RotelModel.RSX1055;
                 break;
             case THING_TYPE_ID_RSX1056:
-                rotelModel = RotelModel.RSX1056;
+                model = RotelModel.RSX1056;
                 break;
             case THING_TYPE_ID_RSX1057:
-                rotelModel = RotelModel.RSX1057;
+                model = RotelModel.RSX1057;
                 break;
             case THING_TYPE_ID_RSX1058:
-                rotelModel = RotelModel.RSX1058;
+                model = RotelModel.RSX1058;
                 break;
             case THING_TYPE_ID_RSX1065:
-                rotelModel = RotelModel.RSX1065;
+                model = RotelModel.RSX1065;
                 break;
             case THING_TYPE_ID_RSX1067:
-                rotelModel = RotelModel.RSX1067;
+                model = RotelModel.RSX1067;
                 break;
             case THING_TYPE_ID_RSX1550:
-                rotelModel = RotelModel.RSX1550;
+                model = RotelModel.RSX1550;
                 break;
             case THING_TYPE_ID_RSX1560:
-                rotelModel = RotelModel.RSX1560;
+                model = RotelModel.RSX1560;
                 break;
             case THING_TYPE_ID_RSX1562:
-                rotelModel = RotelModel.RSX1562;
+                model = RotelModel.RSX1562;
                 break;
             case THING_TYPE_ID_A11:
-                rotelModel = RotelModel.A11;
+                model = RotelModel.A11;
                 break;
             case THING_TYPE_ID_A12:
-                rotelModel = RotelModel.A12;
+                model = RotelModel.A12;
                 break;
             case THING_TYPE_ID_A14:
-                rotelModel = RotelModel.A14;
+                model = RotelModel.A14;
                 break;
             case THING_TYPE_ID_CD11:
-                rotelModel = RotelModel.CD11;
+                model = RotelModel.CD11;
                 break;
             case THING_TYPE_ID_CD14:
-                rotelModel = RotelModel.CD14;
+                model = RotelModel.CD14;
                 break;
             case THING_TYPE_ID_RA11:
-                rotelModel = RotelModel.RA11;
+                model = RotelModel.RA11;
                 break;
             case THING_TYPE_ID_RA12:
-                rotelModel = RotelModel.RA12;
+                model = RotelModel.RA12;
                 break;
             case THING_TYPE_ID_RA1570:
-                rotelModel = RotelModel.RA1570;
+                model = RotelModel.RA1570;
                 break;
             case THING_TYPE_ID_RA1572:
-                rotelModel = RotelModel.RA1572;
+                model = RotelModel.RA1572;
                 break;
             case THING_TYPE_ID_RA1592:
-                rotelModel = RotelModel.RA1592;
+                model = RotelModel.RA1592;
                 break;
             case THING_TYPE_ID_RAP1580:
-                rotelModel = RotelModel.RAP1580;
+                model = RotelModel.RAP1580;
                 break;
             case THING_TYPE_ID_RC1570:
-                rotelModel = RotelModel.RC1570;
+                model = RotelModel.RC1570;
                 break;
             case THING_TYPE_ID_RC1572:
-                rotelModel = RotelModel.RC1572;
+                model = RotelModel.RC1572;
                 break;
             case THING_TYPE_ID_RC1590:
-                rotelModel = RotelModel.RC1590;
+                model = RotelModel.RC1590;
                 break;
             case THING_TYPE_ID_RCD1570:
-                rotelModel = RotelModel.RCD1570;
+                model = RotelModel.RCD1570;
                 break;
             case THING_TYPE_ID_RCD1572:
-                rotelModel = RotelModel.RCD1572;
+                model = RotelModel.RCD1572;
                 break;
             case THING_TYPE_ID_RCX1500:
-                rotelModel = RotelModel.RCX1500;
+                model = RotelModel.RCX1500;
                 break;
             case THING_TYPE_ID_RDD1580:
-                rotelModel = RotelModel.RDD1580;
+                model = RotelModel.RDD1580;
                 break;
             case THING_TYPE_ID_RDG1520:
             case THING_TYPE_ID_RT09:
-                rotelModel = RotelModel.RDG1520;
+                model = RotelModel.RDG1520;
                 break;
             case THING_TYPE_ID_RSP1576:
-                rotelModel = RotelModel.RSP1576;
+                model = RotelModel.RSP1576;
                 break;
             case THING_TYPE_ID_RSP1582:
-                rotelModel = RotelModel.RSP1582;
+                model = RotelModel.RSP1582;
                 break;
             case THING_TYPE_ID_RT11:
-                rotelModel = RotelModel.RT11;
+                model = RotelModel.RT11;
                 break;
             case THING_TYPE_ID_RT1570:
-                rotelModel = RotelModel.RT1570;
+                model = RotelModel.RT1570;
                 break;
             case THING_TYPE_ID_T11:
-                rotelModel = RotelModel.T11;
+                model = RotelModel.T11;
                 break;
             case THING_TYPE_ID_T14:
-                rotelModel = RotelModel.T14;
+                model = RotelModel.T14;
                 break;
             case THING_TYPE_ID_P5:
-                rotelModel = RotelModel.P5;
+                model = RotelModel.P5;
                 break;
             case THING_TYPE_ID_X3:
-                rotelModel = RotelModel.X3;
+                model = RotelModel.X3;
                 break;
             case THING_TYPE_ID_X5:
-                rotelModel = RotelModel.X5;
+                model = RotelModel.X5;
                 break;
             default:
-                rotelModel = DEFAULT_MODEL;
+                model = DEFAULT_MODEL;
                 break;
         }
 
         RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
 
-        RotelProtocol rotelProtocol = RotelProtocol.HEX;
+        protocol = RotelProtocol.HEX;
         if (config.protocol != null && !config.protocol.isEmpty()) {
             try {
-                rotelProtocol = RotelProtocol.getFromName(config.protocol);
+                protocol = RotelProtocol.getFromName(config.protocol);
             } catch (RotelException e) {
+                // Invalid protocol name in configuration, HEX will be considered by default
             }
         } else {
             Map<String, String> properties = editProperties();
             String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
             if (property != null && !property.isEmpty()) {
                 try {
-                    rotelProtocol = RotelProtocol.getFromName(property);
+                    protocol = RotelProtocol.getFromName(property);
                 } catch (RotelException e) {
+                    // Invalid protocol name in thing property, HEX will be considered by default
                 }
             }
         }
-        logger.debug("rotelProtocol {}", rotelProtocol.getName());
+        logger.debug("rotelProtocol {}", protocol.getName());
 
         Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
         Map<RotelSource, String> sourcesLabels = new HashMap<>();
 
         String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
 
-        connector = new RotelSimuConnector(rotelModel, rotelProtocol, sourcesLabels, readerThreadName);
-
-        if (rotelModel.hasVolumeControl()) {
-            maxVolume = rotelModel.getVolumeMax();
-            if (!rotelModel.hasDirectVolumeControl()) {
+        if (model.hasVolumeControl()) {
+            maxVolume = model.getVolumeMax();
+            if (!model.hasDirectVolumeControl()) {
                 logger.info(
                         "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
                         minVolume, maxVolume);
             }
         }
-        if (rotelModel.hasToneControl()) {
-            maxToneLevel = rotelModel.getToneLevelMax();
+        if (model.hasToneControl()) {
+            maxToneLevel = model.getToneLevelMax();
             minToneLevel = -maxToneLevel;
             logger.info(
                     "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
                     minToneLevel, maxToneLevel);
         }
-        if (rotelModel.hasBalanceControl()) {
-            maxBalanceLevel = rotelModel.getBalanceLevelMax();
+        if (model.hasBalanceControl()) {
+            maxBalanceLevel = model.getBalanceLevelMax();
             minBalanceLevel = -maxBalanceLevel;
             logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
                     minBalanceLevel, maxBalanceLevel);
@@ -355,7 +364,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
         if (configError != null) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
         } else {
-            for (RotelSource src : rotelModel.getSources()) {
+            for (RotelSource src : model.getSources()) {
                 // Consider custom input labels
                 String label = null;
                 switch (src.getName()) {
@@ -404,42 +413,49 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
             }
 
+            if (protocol == RotelProtocol.HEX) {
+                protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
+            } else if (protocol == RotelProtocol.ASCII_V1) {
+                protocolHandler = new RotelAsciiV1ProtocolHandler(model);
+            } else {
+                protocolHandler = new RotelAsciiV2ProtocolHandler(model);
+            }
+
             if (USE_SIMULATED_DEVICE) {
-                connector = new RotelSimuConnector(rotelModel, rotelProtocol, sourcesLabels, readerThreadName);
+                connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
             } else if (config.serialPort != null) {
-                connector = new RotelSerialConnector(serialPortManager, config.serialPort, rotelModel, rotelProtocol,
-                        sourcesLabels, readerThreadName);
+                connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
+                        protocolHandler, readerThreadName);
             } else {
-                connector = new RotelIpConnector(config.host, config.port, rotelModel, rotelProtocol, sourcesLabels,
-                        readerThreadName);
+                connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
             }
 
-            if (rotelModel.hasSourceControl()) {
+            if (model.hasSourceControl()) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
-                        getStateOptions(rotelModel.getSources(), sourcesCustomLabels));
+                        getStateOptions(model.getSources(), sourcesCustomLabels));
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
-                        getStateOptions(rotelModel.getSources(), sourcesCustomLabels));
+                        getStateOptions(model.getSources(), sourcesCustomLabels));
                 stateDescriptionProvider.setStateOptions(
                         new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
-                        getStateOptions(rotelModel.getRecordSources(), sourcesCustomLabels));
+                        getStateOptions(model.getRecordSources(), sourcesCustomLabels));
             }
-            if (rotelModel.hasZone2SourceControl()) {
+            if (model.hasZone2SourceControl()) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
-                        getStateOptions(rotelModel.getZone2Sources(), sourcesCustomLabels));
+                        getStateOptions(model.getZone2Sources(), sourcesCustomLabels));
             }
-            if (rotelModel.hasZone3SourceControl()) {
+            if (model.hasZone3SourceControl()) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
-                        getStateOptions(rotelModel.getZone3Sources(), sourcesCustomLabels));
+                        getStateOptions(model.getZone3Sources(), sourcesCustomLabels));
             }
-            if (rotelModel.hasZone4SourceControl()) {
+            if (model.hasZone4SourceControl()) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
-                        getStateOptions(rotelModel.getZone4Sources(), sourcesCustomLabels));
+                        getStateOptions(model.getZone4Sources(), sourcesCustomLabels));
             }
-            if (rotelModel.hasDspControl()) {
+            if (model.hasDspControl()) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
-                        rotelModel.getDspStateOptions());
+                        model.getDspStateOptions());
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
-                        rotelModel.getDspStateOptions());
+                        model.getDspStateOptions());
             }
 
             updateStatus(ThingStatus.UNKNOWN);
@@ -502,20 +518,20 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
                         break;
                     case CHANNEL_ZONE2_POWER:
-                        if (connector.getModel().hasZone2Commands()) {
+                        if (model.hasZone2Commands()) {
                             handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
-                        } else if (connector.getModel().getNbAdditionalZones() == 1) {
+                        } else if (model.getNbAdditionalZones() == 1) {
                             if (isPowerOn() || powerZone2) {
-                                selectZone(2, connector.getModel().getZoneSelectCmd());
+                                selectZone(2, model.getZoneSelectCmd());
                             }
-                            connector.sendCommand(RotelCommand.ZONE_SELECT);
+                            sendCommand(RotelCommand.ZONE_SELECT);
                         } else {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         }
                         break;
                     case CHANNEL_ZONE3_POWER:
-                        if (connector.getModel().hasZone3Commands()) {
+                        if (model.hasZone3Commands()) {
                             handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
                         } else {
                             success = false;
@@ -523,7 +539,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         }
                         break;
                     case CHANNEL_ZONE4_POWER:
-                        if (connector.getModel().hasZone4Commands()) {
+                        if (model.hasZone4Commands()) {
                             handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
                         } else {
                             success = false;
@@ -536,12 +552,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
                         } else {
-                            src = connector.getModel().getSourceFromName(command.toString());
-                            cmd = connector.getModel().hasOtherThanPrimaryCommands() ? src.getMainZoneCommand()
-                                    : src.getCommand();
+                            src = model.getSourceFromName(command.toString());
+                            cmd = model.hasOtherThanPrimaryCommands() ? src.getMainZoneCommand() : src.getCommand();
                             if (cmd != null) {
-                                connector.sendCommand(cmd);
-                                if (connector.getModel().canGetFrequency()) {
+                                sendCommand(cmd);
+                                if (model.canGetFrequency()) {
                                     // send <new-source> returns
                                     // 1.) the selected <new-source>
                                     // 2.) the used frequency
@@ -549,7 +564,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                                     // at response-time the frequency has the value of <old-source>
                                     // so we must wait a short moment to get the frequency of <new-source>
                                     Thread.sleep(1000);
-                                    connector.sendCommand(RotelCommand.FREQUENCY);
+                                    sendCommand(RotelCommand.FREQUENCY);
                                     Thread.sleep(100);
                                     updateChannelState(CHANNEL_FREQUENCY);
                                 }
@@ -564,23 +579,23 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (connector.getModel().hasOtherThanPrimaryCommands()) {
-                            src = connector.getModel().getSourceFromName(command.toString());
+                        } else if (model.hasOtherThanPrimaryCommands()) {
+                            src = model.getSourceFromName(command.toString());
                             cmd = src.getRecordCommand();
                             if (cmd != null) {
-                                connector.sendCommand(cmd);
+                                sendCommand(cmd);
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined record source command",
                                         command, channel);
                             }
                         } else {
-                            src = connector.getModel().getSourceFromName(command.toString());
+                            src = model.getSourceFromName(command.toString());
                             cmd = src.getCommand();
                             if (cmd != null) {
-                                connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
+                                sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
                                 Thread.sleep(100);
-                                connector.sendCommand(cmd);
+                                sendCommand(cmd);
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
@@ -592,22 +607,22 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!powerZone2) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
-                        } else if (connector.getModel().hasZone2Commands()) {
-                            src = connector.getModel().getSourceFromName(command.toString());
+                        } else if (model.hasZone2Commands()) {
+                            src = model.getSourceFromName(command.toString());
                             cmd = src.getZone2Command();
                             if (cmd != null) {
-                                connector.sendCommand(cmd);
+                                sendCommand(cmd);
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
                                         command, channel);
                             }
-                        } else if (connector.getModel().getNbAdditionalZones() >= 1) {
-                            src = connector.getModel().getSourceFromName(command.toString());
+                        } else if (model.getNbAdditionalZones() >= 1) {
+                            src = model.getSourceFromName(command.toString());
                             cmd = src.getCommand();
                             if (cmd != null) {
-                                selectZone(2, connector.getModel().getZoneSelectCmd());
-                                connector.sendCommand(cmd);
+                                selectZone(2, model.getZoneSelectCmd());
+                                sendCommand(cmd);
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
@@ -622,11 +637,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!powerZone3) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
-                        } else if (connector.getModel().hasZone3Commands()) {
-                            src = connector.getModel().getSourceFromName(command.toString());
+                        } else if (model.hasZone3Commands()) {
+                            src = model.getSourceFromName(command.toString());
                             cmd = src.getZone3Command();
                             if (cmd != null) {
-                                connector.sendCommand(cmd);
+                                sendCommand(cmd);
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
@@ -641,11 +656,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!powerZone4) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
-                        } else if (connector.getModel().hasZone4Commands()) {
-                            src = connector.getModel().getSourceFromName(command.toString());
+                        } else if (model.hasZone4Commands()) {
+                            src = model.getSourceFromName(command.toString());
                             cmd = src.getZone4Command();
                             if (cmd != null) {
-                                connector.sendCommand(cmd);
+                                sendCommand(cmd);
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
@@ -662,7 +677,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
                         } else {
-                            connector.sendCommand(connector.getModel().getCommandFromDspName(command.toString()));
+                            sendCommand(model.getCommandFromDspName(command.toString()));
                         }
                         break;
                     case CHANNEL_VOLUME:
@@ -670,7 +685,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (connector.getModel().hasVolumeControl()) {
+                        } else if (model.hasVolumeControl()) {
                             handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
                                     RotelCommand.VOLUME_SET);
                         } else {
@@ -682,7 +697,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (connector.getModel().hasVolumeControl()) {
+                        } else if (model.hasVolumeControl()) {
                             handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
                                     null);
                         } else {
@@ -698,13 +713,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
                                     channel);
-                        } else if (connector.getModel().hasVolumeControl()
-                                && connector.getModel().getNbAdditionalZones() >= 1) {
-                            if (connector.getModel().hasZone2Commands()) {
+                        } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
+                            if (model.hasZone2Commands()) {
                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
                                         RotelCommand.ZONE2_VOLUME_DOWN, RotelCommand.ZONE2_VOLUME_SET);
                             } else {
-                                selectZone(2, connector.getModel().getZoneSelectCmd());
+                                selectZone(2, model.getZoneSelectCmd());
                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
                                         RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
                             }
@@ -721,13 +735,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
                                     channel);
-                        } else if (connector.getModel().hasVolumeControl()
-                                && connector.getModel().getNbAdditionalZones() >= 1) {
-                            if (connector.getModel().hasZone2Commands()) {
+                        } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
+                            if (model.hasZone2Commands()) {
                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
                                         RotelCommand.ZONE2_VOLUME_DOWN, null);
                             } else {
-                                selectZone(2, connector.getModel().getZoneSelectCmd());
+                                selectZone(2, model.getZoneSelectCmd());
                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
                                         RotelCommand.VOLUME_DOWN, null);
                             }
@@ -744,7 +757,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
                                     channel);
-                        } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone3Commands()) {
+                        } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
                             handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
                                     RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
                         } else {
@@ -760,7 +773,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
                                     channel);
-                        } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone4Commands()) {
+                        } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
                             handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
                                     RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
                         } else {
@@ -773,9 +786,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (connector.getModel().hasVolumeControl()) {
-                            handleMuteCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
-                                    getMuteOnCommand(), getMuteOffCommand(), getMuteToggleCommand());
+                        } else if (model.hasVolumeControl()) {
+                            handleMuteCmd(protocol == RotelProtocol.HEX, channel, command, getMuteOnCommand(),
+                                    getMuteOffCommand(), getMuteToggleCommand());
                         } else {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
@@ -785,7 +798,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!powerZone2) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
-                        } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone2Commands()) {
+                        } else if (model.hasVolumeControl() && model.hasZone2Commands()) {
                             handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
                                     RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
                         } else {
@@ -797,7 +810,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!powerZone3) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
-                        } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone3Commands()) {
+                        } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
                             handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
                                     RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
                         } else {
@@ -809,7 +822,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!powerZone4) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
-                        } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone4Commands()) {
+                        } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
                             handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
                                     RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
                         } else {
@@ -850,20 +863,18 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
-                            connector.sendCommand(RotelCommand.PLAY);
+                            sendCommand(RotelCommand.PLAY);
                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
-                            connector.sendCommand(RotelCommand.PAUSE);
-                            if (connector.getProtocol() == RotelProtocol.ASCII_V1
-                                    && connector.getModel() != RotelModel.RCD1570
-                                    && connector.getModel() != RotelModel.RCD1572
-                                    && connector.getModel() != RotelModel.RCX1500) {
+                            sendCommand(RotelCommand.PAUSE);
+                            if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
+                                    && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
                                 Thread.sleep(SLEEP_INTV);
-                                connector.sendCommand(RotelCommand.PLAY_STATUS);
+                                sendCommand(RotelCommand.PLAY_STATUS);
                             }
                         } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
-                            connector.sendCommand(RotelCommand.TRACK_FORWARD);
+                            sendCommand(RotelCommand.TRACK_FORWARD);
                         } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
-                            connector.sendCommand(RotelCommand.TRACK_BACKWORD);
+                            sendCommand(RotelCommand.TRACK_BACKWORD);
                         } else {
                             success = false;
                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
@@ -873,15 +884,14 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (!connector.getModel().hasDimmerControl()) {
+                        } else if (!model.hasDimmerControl()) {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         } else if (command instanceof PercentType) {
                             int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
-                                    * (connector.getModel().getDimmerLevelMax()
-                                            - connector.getModel().getDimmerLevelMin()))
-                                    + connector.getModel().getDimmerLevelMin();
-                            connector.sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
+                                    * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
+                                    + model.getDimmerLevelMin();
+                            sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
                         } else {
                             success = false;
                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
@@ -891,15 +901,14 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (!connector.getModel().hasToneControl()
-                                || connector.getProtocol() == RotelProtocol.HEX) {
+                        } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         } else {
                             handleTcbypassCmd(channel, command,
-                                    connector.getProtocol() == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
+                                    protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
                                             : RotelCommand.TCBYPASS_ON,
-                                    connector.getProtocol() == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
+                                    protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
                                             : RotelCommand.TCBYPASS_OFF);
                         }
                         break;
@@ -907,8 +916,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (!connector.getModel().hasBalanceControl()
-                                || connector.getProtocol() == RotelProtocol.HEX) {
+                        } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         } else {
@@ -921,9 +929,8 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
                         } else {
-                            handleSpeakerCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
-                                    RotelCommand.SPEAKER_A_ON, RotelCommand.SPEAKER_A_OFF,
-                                    RotelCommand.SPEAKER_A_TOGGLE);
+                            handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
+                                    RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
                         }
                         break;
                     case CHANNEL_SPEAKER_B:
@@ -931,9 +938,8 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
                         } else {
-                            handleSpeakerCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
-                                    RotelCommand.SPEAKER_B_ON, RotelCommand.SPEAKER_B_OFF,
-                                    RotelCommand.SPEAKER_B_TOGGLE);
+                            handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
+                                    RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
                         }
                         break;
                     default:
@@ -972,9 +978,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
             throws RotelException {
         if (command instanceof OnOffType && command == OnOffType.ON) {
-            connector.sendCommand(onCmd);
+            sendCommand(onCmd);
         } else if (command instanceof OnOffType && command == OnOffType.OFF) {
-            connector.sendCommand(offCmd);
+            sendCommand(offCmd);
         } else {
             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
         }
@@ -995,22 +1001,22 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
             @Nullable RotelCommand setCmd) throws RotelException {
         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
-            connector.sendCommand(upCmd);
+            sendCommand(upCmd);
         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
-            connector.sendCommand(downCmd);
+            sendCommand(downCmd);
         } else if (command instanceof DecimalType && setCmd == null) {
             int value = ((DecimalType) command).intValue();
             if (value >= minVolume && value <= maxVolume) {
                 if (value > current) {
-                    connector.sendCommand(upCmd);
+                    sendCommand(upCmd);
                 } else if (value < current) {
-                    connector.sendCommand(downCmd);
+                    sendCommand(downCmd);
                 }
             }
         } else if (command instanceof PercentType && setCmd != null) {
             int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
                     + minVolume;
-            connector.sendCommand(setCmd, value);
+            sendCommand(setCmd, value);
         } else {
             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
         }
@@ -1032,11 +1038,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
         if (command instanceof OnOffType) {
             if (onlyToggle) {
-                connector.sendCommand(toggleCmd);
+                sendCommand(toggleCmd);
             } else if (command == OnOffType.ON) {
-                connector.sendCommand(onCmd);
+                sendCommand(onCmd);
             } else if (command == OnOffType.OFF) {
-                connector.sendCommand(offCmd);
+                sendCommand(offCmd);
             }
         } else {
             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
@@ -1061,21 +1067,21 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
             selectToneControl(nbSelect);
-            connector.sendCommand(upCmd);
+            sendCommand(upCmd);
         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
             selectToneControl(nbSelect);
-            connector.sendCommand(downCmd);
+            sendCommand(downCmd);
         } else if (command instanceof DecimalType) {
             int value = ((DecimalType) command).intValue();
             if (value >= minToneLevel && value <= maxToneLevel) {
-                if (connector.getProtocol() != RotelProtocol.HEX) {
-                    connector.sendCommand(setCmd, value);
+                if (protocol != RotelProtocol.HEX) {
+                    sendCommand(setCmd, value);
                 } else if (value > current) {
                     selectToneControl(nbSelect);
-                    connector.sendCommand(upCmd);
+                    sendCommand(upCmd);
                 } else if (value < current) {
                     selectToneControl(nbSelect);
-                    connector.sendCommand(downCmd);
+                    sendCommand(downCmd);
                 }
             }
         } else {
@@ -1097,17 +1103,17 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             throws RotelException, InterruptedException {
         if (command instanceof OnOffType) {
             if (command == OnOffType.ON) {
-                connector.sendCommand(onCmd);
+                sendCommand(onCmd);
                 bass = 0;
                 treble = 0;
                 updateChannelState(CHANNEL_BASS);
                 updateChannelState(CHANNEL_TREBLE);
             } else if (command == OnOffType.OFF) {
-                connector.sendCommand(offCmd);
+                sendCommand(offCmd);
                 Thread.sleep(200);
-                connector.sendCommand(RotelCommand.BASS);
+                sendCommand(RotelCommand.BASS);
                 Thread.sleep(200);
-                connector.sendCommand(RotelCommand.TREBLE);
+                sendCommand(RotelCommand.TREBLE);
             }
         } else {
             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
@@ -1130,11 +1136,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
         if (command instanceof OnOffType) {
             if (onlyToggle) {
-                connector.sendCommand(toggleCmd);
+                sendCommand(toggleCmd);
             } else if (command == OnOffType.ON) {
-                connector.sendCommand(onCmd);
+                sendCommand(onCmd);
             } else if (command == OnOffType.OFF) {
-                connector.sendCommand(offCmd);
+                sendCommand(offCmd);
             }
         } else {
             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
@@ -1156,13 +1162,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
             RotelCommand setCmd) throws RotelException, InterruptedException {
         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
-            connector.sendCommand(rightCmd);
+            sendCommand(rightCmd);
         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
-            connector.sendCommand(leftCmd);
+            sendCommand(leftCmd);
         } else if (command instanceof DecimalType) {
             int value = ((DecimalType) command).intValue();
             if (value >= minBalanceLevel && value <= maxBalanceLevel) {
-                connector.sendCommand(setCmd, value);
+                sendCommand(setCmd, value);
             }
         } else {
             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
@@ -1179,7 +1185,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      */
     private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
         // No tone control select command for RSX-1065
-        if (connector.getProtocol() == RotelProtocol.HEX && connector.getModel() != RotelModel.RSX1065) {
+        if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
             selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
         }
     }
@@ -1195,11 +1201,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      */
     private void selectZone(int zone, @Nullable RotelCommand selectCommand)
             throws RotelException, InterruptedException {
-        if (connector.getProtocol() == RotelProtocol.HEX && connector.getModel().getNbAdditionalZones() >= 1
-                && zone >= 1 && zone != currentZone && selectCommand != null) {
+        if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1 && zone >= 1 && zone != currentZone
+                && selectCommand != null) {
             int nbSelect;
             if (zone < currentZone) {
-                nbSelect = zone + connector.getModel().getNbAdditionalZones() - currentZone;
+                nbSelect = zone + model.getNbAdditionalZones() - currentZone;
                 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
                     nbSelect++;
                 }
@@ -1226,13 +1232,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      */
     private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
             throws RotelException, InterruptedException {
-        if (connector.getProtocol() == RotelProtocol.HEX) {
+        if (protocol == RotelProtocol.HEX) {
             if (preCmd != null) {
-                connector.sendCommand(preCmd);
+                sendCommand(preCmd);
                 Thread.sleep(100);
             }
             for (int i = 1; i <= nbSelect; i++) {
-                connector.sendCommand(selectCmd);
+                sendCommand(selectCmd);
                 Thread.sleep(200);
             }
         }
@@ -1244,7 +1250,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return true if the connection is opened successfully or flase if not
      */
     private synchronized boolean openConnection() {
-        connector.addEventListener(this);
+        protocolHandler.addEventListener(this);
         try {
             connector.open();
         } catch (RotelException e) {
@@ -1259,7 +1265,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      */
     private synchronized void closeConnection() {
         connector.close();
-        connector.removeEventListener(this);
+        protocolHandler.removeEventListener(this);
         logger.debug("closeConnection(): disconnected");
     }
 
@@ -1272,87 +1278,87 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
 
         String key = evt.getKey();
         String value = evt.getValue().trim();
-        if (!RotelConnector.KEY_ERROR.equals(key)) {
+        if (!KEY_ERROR.equals(key)) {
             updateStatus(ThingStatus.ONLINE);
         }
         try {
             switch (key) {
-                case RotelConnector.KEY_ERROR:
+                case KEY_ERROR:
                     logger.debug("Reading feedback message failed");
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                             "@text/offline.comm-error-reading-thread");
                     closeConnection();
                     break;
-                case RotelConnector.KEY_LINE1:
+                case KEY_LINE1:
                     frontPanelLine1 = value;
                     updateChannelState(CHANNEL_LINE1);
                     break;
-                case RotelConnector.KEY_LINE2:
+                case KEY_LINE2:
                     frontPanelLine2 = value;
                     updateChannelState(CHANNEL_LINE2);
                     break;
-                case RotelConnector.KEY_ZONE:
+                case KEY_ZONE:
                     currentZone = Integer.parseInt(value);
                     break;
-                case RotelConnector.KEY_RECORD_SEL:
-                    selectingRecord = RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value);
+                case KEY_RECORD_SEL:
+                    selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
                     break;
-                case RotelConnector.KEY_POWER:
-                    if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
+                case KEY_POWER:
+                    if (POWER_ON.equalsIgnoreCase(value)) {
                         handlePowerOn();
-                    } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
+                    } else if (STANDBY.equalsIgnoreCase(value)) {
                         handlePowerOff();
-                    } else if (RotelConnector.POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
+                    } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
                         schedulePowerOffJob(false);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_POWER_ZONE2:
-                    if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
+                case KEY_POWER_ZONE2:
+                    if (POWER_ON.equalsIgnoreCase(value)) {
                         handlePowerOnZone2();
-                    } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
+                    } else if (STANDBY.equalsIgnoreCase(value)) {
                         handlePowerOffZone2();
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_POWER_ZONE3:
-                    if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
+                case KEY_POWER_ZONE3:
+                    if (POWER_ON.equalsIgnoreCase(value)) {
                         handlePowerOnZone3();
-                    } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
+                    } else if (STANDBY.equalsIgnoreCase(value)) {
                         handlePowerOffZone3();
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_POWER_ZONE4:
-                    if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
+                case KEY_POWER_ZONE4:
+                    if (POWER_ON.equalsIgnoreCase(value)) {
                         handlePowerOnZone4();
-                    } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
+                    } else if (STANDBY.equalsIgnoreCase(value)) {
                         handlePowerOffZone4();
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_VOLUME_MIN:
+                case KEY_VOLUME_MIN:
                     minVolume = Integer.parseInt(value);
-                    if (!connector.getModel().hasDirectVolumeControl()) {
+                    if (!model.hasDirectVolumeControl()) {
                         logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
                                 minVolume);
                     }
                     break;
-                case RotelConnector.KEY_VOLUME_MAX:
+                case KEY_VOLUME_MAX:
                     maxVolume = Integer.parseInt(value);
-                    if (!connector.getModel().hasDirectVolumeControl()) {
+                    if (!model.hasDirectVolumeControl()) {
                         logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
                                 maxVolume);
                     }
                     break;
-                case RotelConnector.KEY_VOLUME:
-                    if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                case KEY_VOLUME:
+                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         volume = minVolume;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         volume = maxVolume;
                     } else {
                         volume = Integer.parseInt(value);
@@ -1361,12 +1367,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     updateChannelState(CHANNEL_MAIN_VOLUME);
                     updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
                     break;
-                case RotelConnector.KEY_MUTE:
-                    if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                case KEY_MUTE:
+                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
                         mute = true;
                         updateChannelState(CHANNEL_MUTE);
                         updateChannelState(CHANNEL_MAIN_MUTE);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         mute = false;
                         updateChannelState(CHANNEL_MUTE);
                         updateChannelState(CHANNEL_MAIN_MUTE);
@@ -1374,13 +1380,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_VOLUME_ZONE2:
+                case KEY_VOLUME_ZONE2:
                     fixedVolumeZone2 = false;
-                    if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
+                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
                         fixedVolumeZone2 = true;
-                    } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         volumeZone2 = minVolume;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         volumeZone2 = maxVolume;
                     } else {
                         volumeZone2 = Integer.parseInt(value);
@@ -1388,76 +1394,76 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     updateChannelState(CHANNEL_ZONE2_VOLUME);
                     updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
                     break;
-                case RotelConnector.KEY_VOLUME_ZONE3:
+                case KEY_VOLUME_ZONE3:
                     fixedVolumeZone3 = false;
-                    if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
+                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
                         fixedVolumeZone3 = true;
-                    } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         volumeZone3 = minVolume;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         volumeZone3 = maxVolume;
                     } else {
                         volumeZone3 = Integer.parseInt(value);
                     }
                     updateChannelState(CHANNEL_ZONE3_VOLUME);
                     break;
-                case RotelConnector.KEY_VOLUME_ZONE4:
+                case KEY_VOLUME_ZONE4:
                     fixedVolumeZone4 = false;
-                    if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
+                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
                         fixedVolumeZone4 = true;
-                    } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         volumeZone4 = minVolume;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         volumeZone4 = maxVolume;
                     } else {
                         volumeZone4 = Integer.parseInt(value);
                     }
                     updateChannelState(CHANNEL_ZONE4_VOLUME);
                     break;
-                case RotelConnector.KEY_MUTE_ZONE2:
-                    if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                case KEY_MUTE_ZONE2:
+                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
                         muteZone2 = true;
                         updateChannelState(CHANNEL_ZONE2_MUTE);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         muteZone2 = false;
                         updateChannelState(CHANNEL_ZONE2_MUTE);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_MUTE_ZONE3:
-                    if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                case KEY_MUTE_ZONE3:
+                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
                         muteZone3 = true;
                         updateChannelState(CHANNEL_ZONE3_MUTE);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         muteZone3 = false;
                         updateChannelState(CHANNEL_ZONE3_MUTE);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_MUTE_ZONE4:
-                    if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                case KEY_MUTE_ZONE4:
+                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
                         muteZone4 = true;
                         updateChannelState(CHANNEL_ZONE4_MUTE);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         muteZone4 = false;
                         updateChannelState(CHANNEL_ZONE4_MUTE);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_TONE_MAX:
+                case KEY_TONE_MAX:
                     maxToneLevel = Integer.parseInt(value);
                     minToneLevel = -maxToneLevel;
                     logger.info(
                             "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
                             minToneLevel, maxToneLevel);
                     break;
-                case RotelConnector.KEY_BASS:
-                    if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                case KEY_BASS:
+                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         bass = minToneLevel;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         bass = maxToneLevel;
                     } else {
                         bass = Integer.parseInt(value);
@@ -1465,10 +1471,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     updateChannelState(CHANNEL_BASS);
                     updateChannelState(CHANNEL_MAIN_BASS);
                     break;
-                case RotelConnector.KEY_TREBLE:
-                    if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                case KEY_TREBLE:
+                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         treble = minToneLevel;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         treble = maxToneLevel;
                     } else {
                         treble = Integer.parseInt(value);
@@ -1476,32 +1482,28 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     updateChannelState(CHANNEL_TREBLE);
                     updateChannelState(CHANNEL_MAIN_TREBLE);
                     break;
-                case RotelConnector.KEY_SOURCE:
-                    source = connector.getModel().getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
+                case KEY_SOURCE:
+                    source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
                     updateChannelState(CHANNEL_SOURCE);
                     updateChannelState(CHANNEL_MAIN_SOURCE);
                     break;
-                case RotelConnector.KEY_RECORD:
-                    recordSource = connector.getModel()
-                            .getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
+                case KEY_RECORD:
+                    recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
                     updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
                     break;
-                case RotelConnector.KEY_SOURCE_ZONE2:
-                    sourceZone2 = connector.getModel()
-                            .getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
+                case KEY_SOURCE_ZONE2:
+                    sourceZone2 = model.getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
                     updateChannelState(CHANNEL_ZONE2_SOURCE);
                     break;
-                case RotelConnector.KEY_SOURCE_ZONE3:
-                    sourceZone3 = connector.getModel()
-                            .getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
+                case KEY_SOURCE_ZONE3:
+                    sourceZone3 = model.getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
                     updateChannelState(CHANNEL_ZONE3_SOURCE);
                     break;
-                case RotelConnector.KEY_SOURCE_ZONE4:
-                    sourceZone4 = connector.getModel()
-                            .getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
+                case KEY_SOURCE_ZONE4:
+                    sourceZone4 = model.getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
                     updateChannelState(CHANNEL_ZONE4_SOURCE);
                     break;
-                case RotelConnector.KEY_DSP_MODE:
+                case KEY_DSP_MODE:
                     if ("dolby_pliix_movie".equals(value)) {
                         value = "dolby_plii_movie";
                     } else if ("dolby_pliix_music".equals(value)) {
@@ -1509,34 +1511,34 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     } else if ("dolby_pliix_game".equals(value)) {
                         value = "dolby_plii_game";
                     }
-                    dsp = connector.getModel().getDspFromFeedback(value);
+                    dsp = model.getDspFromFeedback(value);
                     logger.debug("DSP {}", dsp.getName());
                     updateChannelState(CHANNEL_DSP);
                     updateChannelState(CHANNEL_MAIN_DSP);
                     break;
-                case RotelConnector.KEY1_PLAY_STATUS:
-                case RotelConnector.KEY2_PLAY_STATUS:
-                    if (RotelConnector.PLAY.equalsIgnoreCase(value)) {
+                case KEY1_PLAY_STATUS:
+                case KEY2_PLAY_STATUS:
+                    if (PLAY.equalsIgnoreCase(value)) {
                         playStatus = RotelPlayStatus.PLAYING;
                         updateChannelState(CHANNEL_PLAY_CONTROL);
-                    } else if (RotelConnector.PAUSE.equalsIgnoreCase(value)) {
+                    } else if (PAUSE.equalsIgnoreCase(value)) {
                         playStatus = RotelPlayStatus.PAUSED;
                         updateChannelState(CHANNEL_PLAY_CONTROL);
-                    } else if (RotelConnector.STOP.equalsIgnoreCase(value)) {
+                    } else if (STOP.equalsIgnoreCase(value)) {
                         playStatus = RotelPlayStatus.STOPPED;
                         updateChannelState(CHANNEL_PLAY_CONTROL);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_TRACK:
-                    if (source.getName().equals("CD") && !connector.getModel().hasSourceControl()) {
+                case KEY_TRACK:
+                    if (source.getName().equals("CD") && !model.hasSourceControl()) {
                         track = Integer.parseInt(value);
                         updateChannelState(CHANNEL_TRACK);
                     }
                     break;
-                case RotelConnector.KEY_FREQ:
-                    if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                case KEY_FREQ:
+                    if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         frequency = 0.0;
                     } else {
                         // Suppress a potential ending "k" or "K"
@@ -1547,39 +1549,39 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     }
                     updateChannelState(CHANNEL_FREQUENCY);
                     break;
-                case RotelConnector.KEY_DIMMER:
+                case KEY_DIMMER:
                     brightness = Integer.parseInt(value);
                     updateChannelState(CHANNEL_BRIGHTNESS);
                     break;
-                case RotelConnector.KEY_UPDATE_MODE:
-                case RotelConnector.KEY_DISPLAY_UPDATE:
+                case KEY_UPDATE_MODE:
+                case KEY_DISPLAY_UPDATE:
                     break;
-                case RotelConnector.KEY_TONE:
-                    if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                case KEY_TONE:
+                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
                         tcbypass = false;
                         updateChannelState(CHANNEL_TCBYPASS);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         tcbypass = true;
                         updateChannelState(CHANNEL_TCBYPASS);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_TCBYPASS:
-                    if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                case KEY_TCBYPASS:
+                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
                         tcbypass = true;
                         updateChannelState(CHANNEL_TCBYPASS);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         tcbypass = false;
                         updateChannelState(CHANNEL_TCBYPASS);
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case RotelConnector.KEY_BALANCE:
-                    if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                case KEY_BALANCE:
+                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
                         balance = minBalanceLevel;
-                    } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
                         balance = maxBalanceLevel;
                     } else if (value.toUpperCase().startsWith("L")) {
                         balance = -Integer.parseInt(value.substring(1));
@@ -1590,23 +1592,23 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     }
                     updateChannelState(CHANNEL_BALANCE);
                     break;
-                case RotelConnector.KEY_SPEAKER:
-                    if (RotelConnector.MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
+                case KEY_SPEAKER:
+                    if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
                         speakera = true;
                         speakerb = false;
                         updateChannelState(CHANNEL_SPEAKER_A);
                         updateChannelState(CHANNEL_SPEAKER_B);
-                    } else if (RotelConnector.MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
                         speakera = false;
                         speakerb = true;
                         updateChannelState(CHANNEL_SPEAKER_A);
                         updateChannelState(CHANNEL_SPEAKER_B);
-                    } else if (RotelConnector.MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
                         speakera = true;
                         speakerb = true;
                         updateChannelState(CHANNEL_SPEAKER_A);
                         updateChannelState(CHANNEL_SPEAKER_B);
-                    } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
                         speakera = false;
                         speakerb = false;
                         updateChannelState(CHANNEL_SPEAKER_A);
@@ -1782,37 +1784,36 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             synchronized (sequenceLock) {
                 logger.debug("Power ON job");
                 try {
-                    switch (connector.getProtocol()) {
+                    switch (protocol) {
                         case HEX:
-                            if (connector.getModel().getRespNbChars() <= 13
-                                    && connector.getModel().hasVolumeControl()) {
-                                connector.sendCommand(getVolumeDownCommand());
+                            if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
+                                sendCommand(getVolumeDownCommand());
                                 Thread.sleep(100);
-                                connector.sendCommand(getVolumeUpCommand());
+                                sendCommand(getVolumeUpCommand());
                                 Thread.sleep(100);
                             }
-                            if (connector.getModel().getNbAdditionalZones() >= 1) {
-                                if (currentZone != 1 && connector.getModel()
-                                        .getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
-                                    selectZone(1, connector.getModel().getZoneSelectCmd());
+                            if (model.getNbAdditionalZones() >= 1) {
+                                if (currentZone != 1
+                                        && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
+                                    selectZone(1, model.getZoneSelectCmd());
                                 } else if (!selectingRecord) {
-                                    connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
+                                    sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
                                     Thread.sleep(100);
                                 }
                             } else {
-                                connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
+                                sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
                                 Thread.sleep(100);
                             }
-                            if (connector.getModel().hasToneControl()) {
-                                if (connector.getModel() == RotelModel.RSX1065) {
+                            if (model.hasToneControl()) {
+                                if (model == RotelModel.RSX1065) {
                                     // No tone control select command
-                                    connector.sendCommand(RotelCommand.TREBLE_DOWN);
+                                    sendCommand(RotelCommand.TREBLE_DOWN);
                                     Thread.sleep(100);
-                                    connector.sendCommand(RotelCommand.TREBLE_UP);
+                                    sendCommand(RotelCommand.TREBLE_UP);
                                     Thread.sleep(100);
-                                    connector.sendCommand(RotelCommand.BASS_DOWN);
+                                    sendCommand(RotelCommand.BASS_DOWN);
                                     Thread.sleep(100);
-                                    connector.sendCommand(RotelCommand.BASS_UP);
+                                    sendCommand(RotelCommand.BASS_UP);
                                     Thread.sleep(100);
                                 } else {
                                     selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
@@ -1820,131 +1821,125 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             }
                             break;
                         case ASCII_V1:
-                            if (connector.getModel() != RotelModel.RAP1580 && connector.getModel() != RotelModel.RDD1580
-                                    && connector.getModel() != RotelModel.RSP1576
-                                    && connector.getModel() != RotelModel.RSP1582) {
-                                connector.sendCommand(RotelCommand.UPDATE_AUTO);
+                            if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
+                                    && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
+                                sendCommand(RotelCommand.UPDATE_AUTO);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasSourceControl()) {
-                                connector.sendCommand(RotelCommand.SOURCE);
+                            if (model.hasSourceControl()) {
+                                sendCommand(RotelCommand.SOURCE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasVolumeControl() || connector.getModel().hasToneControl()) {
-                                if (connector.getModel().hasVolumeControl()
-                                        && connector.getModel() != RotelModel.RAP1580
-                                        && connector.getModel() != RotelModel.RSP1576
-                                        && connector.getModel() != RotelModel.RSP1582) {
-                                    connector.sendCommand(RotelCommand.VOLUME_GET_MIN);
+                            if (model.hasVolumeControl() || model.hasToneControl()) {
+                                if (model.hasVolumeControl() && model != RotelModel.RAP1580
+                                        && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
+                                    sendCommand(RotelCommand.VOLUME_GET_MIN);
                                     Thread.sleep(SLEEP_INTV);
-                                    connector.sendCommand(RotelCommand.VOLUME_GET_MAX);
+                                    sendCommand(RotelCommand.VOLUME_GET_MAX);
                                     Thread.sleep(SLEEP_INTV);
                                 }
-                                if (connector.getModel().hasToneControl()) {
-                                    connector.sendCommand(RotelCommand.TONE_MAX);
+                                if (model.hasToneControl()) {
+                                    sendCommand(RotelCommand.TONE_MAX);
                                     Thread.sleep(SLEEP_INTV);
                                 }
                                 // Wait enough to be sure to get the min/max values requested just before
                                 Thread.sleep(250);
-                                if (connector.getModel().hasVolumeControl()) {
-                                    connector.sendCommand(RotelCommand.VOLUME_GET);
+                                if (model.hasVolumeControl()) {
+                                    sendCommand(RotelCommand.VOLUME_GET);
                                     Thread.sleep(SLEEP_INTV);
-                                    if (connector.getModel() != RotelModel.RA11
-                                            && connector.getModel() != RotelModel.RA12
-                                            && connector.getModel() != RotelModel.RCX1500) {
-                                        connector.sendCommand(RotelCommand.MUTE);
+                                    if (model != RotelModel.RA11 && model != RotelModel.RA12
+                                            && model != RotelModel.RCX1500) {
+                                        sendCommand(RotelCommand.MUTE);
                                         Thread.sleep(SLEEP_INTV);
                                     }
                                 }
-                                if (connector.getModel().hasToneControl()) {
-                                    connector.sendCommand(RotelCommand.BASS);
+                                if (model.hasToneControl()) {
+                                    sendCommand(RotelCommand.BASS);
                                     Thread.sleep(SLEEP_INTV);
-                                    connector.sendCommand(RotelCommand.TREBLE);
+                                    sendCommand(RotelCommand.TREBLE);
                                     Thread.sleep(SLEEP_INTV);
-                                    connector.sendCommand(RotelCommand.TONE_CONTROLS);
+                                    sendCommand(RotelCommand.TONE_CONTROLS);
                                     Thread.sleep(SLEEP_INTV);
                                 }
                             }
-                            if (connector.getModel().hasBalanceControl()) {
-                                connector.sendCommand(RotelCommand.BALANCE);
+                            if (model.hasBalanceControl()) {
+                                sendCommand(RotelCommand.BALANCE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasPlayControl()) {
-                                if (connector.getModel() != RotelModel.RCD1570
-                                        && connector.getModel() != RotelModel.RCD1572
-                                        && (connector.getModel() != RotelModel.RCX1500
-                                                || !source.getName().equals("CD"))) {
-                                    connector.sendCommand(RotelCommand.PLAY_STATUS);
+                            if (model.hasPlayControl()) {
+                                if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
+                                        && (model != RotelModel.RCX1500 || !source.getName().equals("CD"))) {
+                                    sendCommand(RotelCommand.PLAY_STATUS);
                                     Thread.sleep(SLEEP_INTV);
                                 } else {
-                                    connector.sendCommand(RotelCommand.CD_PLAY_STATUS);
+                                    sendCommand(RotelCommand.CD_PLAY_STATUS);
                                     Thread.sleep(SLEEP_INTV);
                                 }
                             }
-                            if (connector.getModel().hasDspControl()) {
-                                connector.sendCommand(RotelCommand.DSP_MODE);
+                            if (model.hasDspControl()) {
+                                sendCommand(RotelCommand.DSP_MODE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().canGetFrequency()) {
-                                connector.sendCommand(RotelCommand.FREQUENCY);
+                            if (model.canGetFrequency()) {
+                                sendCommand(RotelCommand.FREQUENCY);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasDimmerControl() && connector.getModel().canGetDimmerLevel()) {
-                                connector.sendCommand(RotelCommand.DIMMER_LEVEL_GET);
+                            if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
+                                sendCommand(RotelCommand.DIMMER_LEVEL_GET);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasSpeakerGroups()) {
-                                connector.sendCommand(RotelCommand.SPEAKER);
+                            if (model.hasSpeakerGroups()) {
+                                sendCommand(RotelCommand.SPEAKER);
                                 Thread.sleep(SLEEP_INTV);
                             }
                             break;
                         case ASCII_V2:
-                            connector.sendCommand(RotelCommand.UPDATE_AUTO);
+                            sendCommand(RotelCommand.UPDATE_AUTO);
                             Thread.sleep(SLEEP_INTV);
-                            if (connector.getModel().hasSourceControl()) {
-                                connector.sendCommand(RotelCommand.SOURCE);
+                            if (model.hasSourceControl()) {
+                                sendCommand(RotelCommand.SOURCE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasVolumeControl()) {
-                                connector.sendCommand(RotelCommand.VOLUME_GET);
+                            if (model.hasVolumeControl()) {
+                                sendCommand(RotelCommand.VOLUME_GET);
                                 Thread.sleep(SLEEP_INTV);
-                                connector.sendCommand(RotelCommand.MUTE);
+                                sendCommand(RotelCommand.MUTE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasToneControl()) {
-                                connector.sendCommand(RotelCommand.BASS);
+                            if (model.hasToneControl()) {
+                                sendCommand(RotelCommand.BASS);
                                 Thread.sleep(SLEEP_INTV);
-                                connector.sendCommand(RotelCommand.TREBLE);
+                                sendCommand(RotelCommand.TREBLE);
                                 Thread.sleep(SLEEP_INTV);
-                                connector.sendCommand(RotelCommand.TCBYPASS);
+                                sendCommand(RotelCommand.TCBYPASS);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasBalanceControl()) {
-                                connector.sendCommand(RotelCommand.BALANCE);
+                            if (model.hasBalanceControl()) {
+                                sendCommand(RotelCommand.BALANCE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasPlayControl()) {
-                                connector.sendCommand(RotelCommand.PLAY_STATUS);
+                            if (model.hasPlayControl()) {
+                                sendCommand(RotelCommand.PLAY_STATUS);
                                 Thread.sleep(SLEEP_INTV);
-                                if (source.getName().equals("CD") && !connector.getModel().hasSourceControl()) {
-                                    connector.sendCommand(RotelCommand.TRACK);
+                                if (source.getName().equals("CD") && !model.hasSourceControl()) {
+                                    sendCommand(RotelCommand.TRACK);
                                     Thread.sleep(SLEEP_INTV);
                                 }
                             }
-                            if (connector.getModel().hasDspControl()) {
-                                connector.sendCommand(RotelCommand.DSP_MODE);
+                            if (model.hasDspControl()) {
+                                sendCommand(RotelCommand.DSP_MODE);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().canGetFrequency()) {
-                                connector.sendCommand(RotelCommand.FREQUENCY);
+                            if (model.canGetFrequency()) {
+                                sendCommand(RotelCommand.FREQUENCY);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasDimmerControl() && connector.getModel().canGetDimmerLevel()) {
-                                connector.sendCommand(RotelCommand.DIMMER_LEVEL_GET);
+                            if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
+                                sendCommand(RotelCommand.DIMMER_LEVEL_GET);
                                 Thread.sleep(SLEEP_INTV);
                             }
-                            if (connector.getModel().hasSpeakerGroups()) {
-                                connector.sendCommand(RotelCommand.SPEAKER);
+                            if (model.hasSpeakerGroups()) {
+                                sendCommand(RotelCommand.SPEAKER);
                                 Thread.sleep(SLEEP_INTV);
                             }
                             break;
@@ -1983,14 +1978,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             synchronized (sequenceLock) {
                 logger.debug("Power ON zone 2 job");
                 try {
-                    if (connector.getProtocol() == RotelProtocol.HEX
-                            && connector.getModel().getNbAdditionalZones() >= 1) {
-                        selectZone(2, connector.getModel().getZoneSelectCmd());
-                        connector.sendCommand(connector.getModel().hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN
-                                : RotelCommand.VOLUME_DOWN);
+                    if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1) {
+                        selectZone(2, model.getZoneSelectCmd());
+                        sendCommand(
+                                model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
                         Thread.sleep(100);
-                        connector.sendCommand(connector.getModel().hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP
-                                : RotelCommand.VOLUME_UP);
+                        sendCommand(model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP : RotelCommand.VOLUME_UP);
                         Thread.sleep(100);
                     }
                 } catch (RotelException e) {
@@ -2027,14 +2020,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             synchronized (sequenceLock) {
                 logger.debug("Power ON zone 3 job");
                 try {
-                    if (connector.getProtocol() == RotelProtocol.HEX
-                            && connector.getModel().getNbAdditionalZones() >= 2) {
-                        selectZone(3, connector.getModel().getZoneSelectCmd());
-                        connector.sendCommand(connector.getModel().hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN
-                                : RotelCommand.VOLUME_DOWN);
+                    if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 2) {
+                        selectZone(3, model.getZoneSelectCmd());
+                        sendCommand(
+                                model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
                         Thread.sleep(100);
-                        connector.sendCommand(connector.getModel().hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP
-                                : RotelCommand.VOLUME_UP);
+                        sendCommand(model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP : RotelCommand.VOLUME_UP);
                         Thread.sleep(100);
                     }
                 } catch (RotelException e) {
@@ -2071,14 +2062,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             synchronized (sequenceLock) {
                 logger.debug("Power ON zone 4 job");
                 try {
-                    if (connector.getProtocol() == RotelProtocol.HEX
-                            && connector.getModel().getNbAdditionalZones() >= 3) {
-                        selectZone(4, connector.getModel().getZoneSelectCmd());
-                        connector.sendCommand(connector.getModel().hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN
-                                : RotelCommand.VOLUME_DOWN);
+                    if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 3) {
+                        selectZone(4, model.getZoneSelectCmd());
+                        sendCommand(
+                                model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
                         Thread.sleep(100);
-                        connector.sendCommand(connector.getModel().hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP
-                                : RotelCommand.VOLUME_UP);
+                        sendCommand(model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP : RotelCommand.VOLUME_UP);
                         Thread.sleep(100);
                     }
                 } catch (RotelException e) {
@@ -2121,7 +2110,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     synchronized (sequenceLock) {
                         schedulePowerOffJob(true);
                         try {
-                            connector.sendCommand(connector.getModel().getPowerStateCmd());
+                            sendCommand(model.getPowerStateCmd());
                         } catch (RotelException e) {
                             error = "@text/offline.comm-error-first-command-after-reconnection";
                             logger.debug("First command after connection failed", e);
@@ -2321,11 +2310,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 state = new StringType(frontPanelLine2);
                 break;
             case CHANNEL_BRIGHTNESS:
-                if (isPowerOn() && connector.getModel().hasDimmerControl()) {
-                    long dimmerPct = Math.round((double) (brightness - connector.getModel().getDimmerLevelMin())
-                            / (double) (connector.getModel().getDimmerLevelMax()
-                                    - connector.getModel().getDimmerLevelMin())
-                            * 100.0);
+                if (isPowerOn() && model.hasDimmerControl()) {
+                    long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
+                            / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
                     state = new PercentType(BigDecimal.valueOf(dimmerPct));
                 }
                 break;
@@ -2371,8 +2358,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getPowerOnCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON
-                : RotelCommand.POWER_ON;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
     }
 
     /**
@@ -2381,8 +2367,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getPowerOffCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF
-                : RotelCommand.POWER_OFF;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
     }
 
     /**
@@ -2391,8 +2376,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getVolumeUpCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP
-                : RotelCommand.VOLUME_UP;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
     }
 
     /**
@@ -2401,8 +2385,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getVolumeDownCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
-                : RotelCommand.VOLUME_DOWN;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN : RotelCommand.VOLUME_DOWN;
     }
 
     /**
@@ -2411,8 +2394,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getMuteOnCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON
-                : RotelCommand.MUTE_ON;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
     }
 
     /**
@@ -2421,8 +2403,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getMuteOffCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF
-                : RotelCommand.MUTE_OFF;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
     }
 
     /**
@@ -2431,7 +2412,38 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * @return the command
      */
     private RotelCommand getMuteToggleCommand() {
-        return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
-                : RotelCommand.MUTE_TOGGLE;
+        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE : RotelCommand.MUTE_TOGGLE;
+    }
+
+    private void sendCommand(RotelCommand cmd) throws RotelException {
+        sendCommand(cmd, null);
+    }
+
+    /**
+     * Request the Rotel device to execute a command
+     *
+     * @param cmd the command to execute
+     * @param value the integer value to consider for volume, bass or treble adjustment
+     *
+     * @throws RotelException - In case of any problem
+     */
+    private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
+        byte[] message;
+        try {
+            message = protocolHandler.buildCommandMessage(cmd, value);
+        } catch (RotelException e) {
+            // Command not supported
+            logger.debug("sendCommand: {}", e.getMessage());
+            return;
+        }
+        connector.writeOutput(cmd.getName(), message);
+
+        if (connector instanceof RotelSimuConnector) {
+            if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
+                    || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
+                    || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
+                ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);
+            }
+        }
     }
 }
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelAbstractProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelAbstractProtocolHandler.java
new file mode 100644 (file)
index 0000000..db90246
--- /dev/null
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol;
+
+import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.rotel.internal.RotelException;
+import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.communication.RotelCommand;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class for handling a Rotel protocol (build of command messages, decoding of incoming data)
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public abstract class RotelAbstractProtocolHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(RotelAbstractProtocolHandler.class);
+
+    protected final RotelModel model;
+
+    private final List<RotelMessageEventListener> listeners = new ArrayList<>();
+
+    /**
+     * Constructor
+     *
+     * @param model the Rotel model in use
+     */
+    public RotelAbstractProtocolHandler(RotelModel model) {
+        this.model = model;
+    }
+
+    public abstract RotelProtocol getProtocol();
+
+    /**
+     * Build the message associated to a Rotel command
+     *
+     * @param cmd the command to execute
+     * @param value the integer value to consider for volume, bass or treble adjustment
+     *
+     * @throws RotelException - In case the command is not supported by the protocol
+     */
+    public abstract byte[] buildCommandMessage(RotelCommand cmd, @Nullable Integer value) throws RotelException;
+
+    public abstract void handleIncomingData(byte[] inDataBuffer, int length);
+
+    public void handleInIncomingError() {
+        dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
+    }
+
+    /**
+     * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
+     *
+     * @param incomingMessage the received message
+     */
+    protected void handleIncomingMessage(byte[] incomingMessage) {
+        logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
+
+        try {
+            validateResponse(incomingMessage);
+        } catch (RotelException e) {
+            return;
+        }
+
+        handleValidMessage(incomingMessage);
+    }
+
+    /**
+     * Validate the content of a feedback message
+     *
+     * @param responseMessage the buffer containing the feedback message
+     *
+     * @throws RotelException - If the message has unexpected content
+     */
+    protected abstract void validateResponse(byte[] responseMessage) throws RotelException;
+
+    /**
+     * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
+     *
+     * @param incomingMessage the received message
+     */
+    protected abstract void handleValidMessage(byte[] incomingMessage);
+
+    /**
+     * Add a listener to the list of listeners to be notified with events
+     *
+     * @param listener the listener
+     */
+    public void addEventListener(RotelMessageEventListener listener) {
+        listeners.add(listener);
+    }
+
+    /**
+     * Remove a listener from the list of listeners to be notified with events
+     *
+     * @param listener the listener
+     */
+    public void removeEventListener(RotelMessageEventListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Dispatch an event (key, value) to the event listeners
+     *
+     * @param key the key
+     * @param value the value
+     */
+    protected void dispatchKeyValue(String key, String value) {
+        RotelMessageEvent event = new RotelMessageEvent(this, key, value);
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).onNewMessageEvent(event);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelMessageEvent.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelMessageEvent.java
new file mode 100644 (file)
index 0000000..5b33c86
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol;
+
+import java.util.EventObject;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Rotel event used to notify changes coming from messages received from the Rotel device
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class RotelMessageEvent extends EventObject {
+
+    private static final long serialVersionUID = 1L;
+    private String key;
+    private String value;
+
+    public RotelMessageEvent(Object source, String key, String value) {
+        super(source);
+        this.key = key;
+        this.value = value;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelMessageEventListener.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelMessageEventListener.java
new file mode 100644 (file)
index 0000000..93dae84
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol;
+
+import java.util.EventListener;
+import java.util.EventObject;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Rotel Event Listener interface. Handles incoming Rotel message events
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface RotelMessageEventListener extends EventListener {
+
+    /**
+     * Event handler method for incoming Rotel message events
+     *
+     * @param event the event object
+     */
+    public void onNewMessageEvent(EventObject event);
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelProtocol.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/RotelProtocol.java
new file mode 100644 (file)
index 0000000..0445406
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.rotel.internal.RotelException;
+
+/**
+ * Represents the different kinds of protocols
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public enum RotelProtocol {
+
+    HEX("HEX"),
+    ASCII_V1("ASCII_V1"),
+    ASCII_V2("ASCII_V2");
+
+    private String name;
+
+    /**
+     * Constructor
+     *
+     * @param name the protocol name
+     */
+    private RotelProtocol(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Get the protocol name
+     *
+     * @return the protocol name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Get the protocol associated to a name
+     *
+     * @param name the name used to identify the protocol
+     *
+     * @return the protocol associated to the searched name
+     *
+     * @throws RotelException - If no protocol is associated to the searched name
+     */
+    public static RotelProtocol getFromName(String name) throws RotelException {
+        for (RotelProtocol value : RotelProtocol.values()) {
+            if (value.getName().equals(name)) {
+                return value;
+            }
+        }
+        throw new RotelException("Invalid protocol name: " + name);
+    }
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java
new file mode 100644 (file)
index 0000000..71cd0f8
--- /dev/null
@@ -0,0 +1,195 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol.ascii;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.regex.PatternSyntaxException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.rotel.internal.RotelException;
+import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class for handling a Rotel ASCII protocol (build of command messages, decoding of incoming data)
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractProtocolHandler {
+
+    /** Special characters that can be found in the feedback messages for several devices using the ASCII protocol */
+    public static final byte[][] SPECIAL_CHARACTERS = { { (byte) 0xEE, (byte) 0x82, (byte) 0x85 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x84 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x92 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x87 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8E },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x89 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x93 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x8C }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8F },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x8A }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8B },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x81 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x82 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x83 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x94 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x97 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x98 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x80 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x99 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x9A }, { (byte) 0xEE, (byte) 0x82, (byte) 0x88 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x95 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x96 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x90 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x91 },
+            { (byte) 0xEE, (byte) 0x82, (byte) 0x8D }, { (byte) 0xEE, (byte) 0x80, (byte) 0x80, (byte) 0xEE,
+                    (byte) 0x80, (byte) 0x81, (byte) 0xEE, (byte) 0x80, (byte) 0x82 } };
+
+    /** Special characters that can be found in the feedback messages for the RCD-1572 */
+    public static final byte[][] SPECIAL_CHARACTERS_RCD1572 = { { (byte) 0xC2, (byte) 0x8C },
+            { (byte) 0xC2, (byte) 0x54 }, { (byte) 0xC2, (byte) 0x81 }, { (byte) 0xC2, (byte) 0x82 },
+            { (byte) 0xC2, (byte) 0x83 } };
+
+    /** Empty table of special characters */
+    public static final byte[][] NO_SPECIAL_CHARACTERS = {};
+
+    private final Logger logger = LoggerFactory.getLogger(RotelAbstractAsciiProtocolHandler.class);
+
+    private final char terminatingChar;
+    private final int size;
+    private final byte[] dataBuffer;
+
+    private int index;
+
+    /**
+     * Constructor
+     *
+     * @param model the Rotel model in use
+     * @param protocol the protocol to be used
+     */
+    public RotelAbstractAsciiProtocolHandler(RotelModel model, char terminatingChar) {
+        super(model);
+        this.terminatingChar = terminatingChar;
+        this.size = 64;
+        this.dataBuffer = new byte[size];
+        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;
+            }
+        }
+    }
+
+    /**
+     * Validate the content of a feedback message
+     *
+     * @param responseMessage the buffer containing the feedback message
+     *
+     * @throws RotelException - If the message has unexpected content
+     */
+    @Override
+    protected void validateResponse(byte[] responseMessage) throws RotelException {
+        // Check minimum message length
+        if (responseMessage.length < 1) {
+            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");
+        }
+    }
+
+    /**
+     * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
+     *
+     * @param incomingMessage the received message
+     */
+    @Override
+    protected void handleValidMessage(byte[] incomingMessage) {
+        byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
+
+        // Replace characters with code < 32 by a space before converting to a string
+        for (int i = 0; i < message.length; i++) {
+            if (message[i] < 0x20) {
+                message[i] = 0x20;
+            }
+        }
+
+        String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII);
+        logger.debug("handleValidAsciiMessage: chars *{}*", value);
+        value = value.trim();
+        if (value.isEmpty()) {
+            return;
+        }
+        try {
+            String[] splittedValue = value.split("=");
+            if (splittedValue.length != 2) {
+                logger.debug("handleValidAsciiMessage: ignored message {}", value);
+            } else {
+                dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
+            }
+        } catch (PatternSyntaxException e) {
+            logger.debug("handleValidAsciiMessage: ignored message {}", value);
+        }
+    }
+
+    /**
+     * Suppress certain sequences of bytes from a message
+     *
+     * @param message the message as a table of bytes
+     * @param bytesSequences the table containing the sequence of bytes to be ignored
+     *
+     * @return the message without the unexpected sequence of bytes
+     */
+    private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
+        if (bytesSequences.length == 0) {
+            return message;
+        }
+        byte[] filteredMsg = new byte[message.length];
+        int srcIdx = 0;
+        int dstIdx = 0;
+        while (srcIdx < message.length) {
+            int ignoredLength = 0;
+            for (int i = 0; i < bytesSequences.length; i++) {
+                int size = bytesSequences[i].length;
+                if ((message.length - srcIdx) >= size) {
+                    boolean match = true;
+                    for (int j = 0; j < size; j++) {
+                        if (message[srcIdx + j] != bytesSequences[i][j]) {
+                            match = false;
+                            break;
+                        }
+                    }
+                    if (match) {
+                        ignoredLength = size;
+                        break;
+                    }
+                }
+            }
+            if (ignoredLength > 0) {
+                srcIdx += ignoredLength;
+            } else {
+                filteredMsg[dstIdx++] = message[srcIdx++];
+            }
+        }
+        return Arrays.copyOf(filteredMsg, dstIdx);
+    }
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java
new file mode 100644 (file)
index 0000000..5e47063
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol.ascii;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.rotel.internal.RotelException;
+import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.communication.RotelCommand;
+import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for handling the Rotel ASCII V1 protocol (build of command messages, decoding of incoming data)
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class RotelAsciiV1ProtocolHandler extends RotelAbstractAsciiProtocolHandler {
+
+    private static final char CHAR_END_RESPONSE = '!';
+
+    private final Logger logger = LoggerFactory.getLogger(RotelAsciiV1ProtocolHandler.class);
+
+    /**
+     * Constructor
+     *
+     * @param model the Rotel model in use
+     */
+    public RotelAsciiV1ProtocolHandler(RotelModel model) {
+        super(model, CHAR_END_RESPONSE);
+    }
+
+    @Override
+    public RotelProtocol getProtocol() {
+        return RotelProtocol.ASCII_V1;
+    }
+
+    @Override
+    public byte[] buildCommandMessage(RotelCommand cmd, @Nullable Integer value) throws RotelException {
+        String messageStr = cmd.getAsciiCommandV1();
+        if (messageStr == null) {
+            throw new RotelException("Command \"" + cmd.getName() + "\" ignored: not available for ASCII V1 protocol");
+        }
+        if (value != null) {
+            switch (cmd) {
+                case VOLUME_SET:
+                    messageStr += String.format("%d", value);
+                    break;
+                case BASS_SET:
+                case TREBLE_SET:
+                    if (value == 0) {
+                        messageStr += "000";
+                    } else if (value > 0) {
+                        messageStr += String.format("+%02d", value);
+                    } else {
+                        messageStr += String.format("-%02d", -value);
+                    }
+                    break;
+                case BALANCE_SET:
+                    if (value == 0) {
+                        messageStr += "000";
+                    } else if (value > 0) {
+                        messageStr += String.format("R%02d", value);
+                    } else {
+                        messageStr += String.format("L%02d", -value);
+                    }
+                    break;
+                case DIMMER_LEVEL_SET:
+                    if (value > 0 && model.getDimmerLevelMin() < 0) {
+                        messageStr += String.format("+%d", value);
+                    } else {
+                        messageStr += String.format("%d", value);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+        if (!messageStr.endsWith("?")) {
+            messageStr += "!";
+        }
+        byte[] message = messageStr.getBytes(StandardCharsets.US_ASCII);
+        logger.debug("Command \"{}\" => {}", cmd.getName(), messageStr);
+        return message;
+    }
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java
new file mode 100644 (file)
index 0000000..c90f5a8
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol.ascii;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.rotel.internal.RotelException;
+import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.communication.RotelCommand;
+import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for handling the Rotel ASCII V2 protocol (build of command messages, decoding of incoming data)
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandler {
+
+    private static final char CHAR_END_RESPONSE = '$';
+
+    private final Logger logger = LoggerFactory.getLogger(RotelAsciiV2ProtocolHandler.class);
+
+    /**
+     * Constructor
+     *
+     * @param model the Rotel model in use
+     */
+    public RotelAsciiV2ProtocolHandler(RotelModel model) {
+        super(model, CHAR_END_RESPONSE);
+    }
+
+    @Override
+    public RotelProtocol getProtocol() {
+        return RotelProtocol.ASCII_V2;
+    }
+
+    @Override
+    public byte[] buildCommandMessage(RotelCommand cmd, @Nullable Integer value) throws RotelException {
+        String messageStr = cmd.getAsciiCommandV2();
+        if (messageStr == null) {
+            throw new RotelException("Command \"" + cmd.getName() + "\" ignored: not available for ASCII V2 protocol");
+        }
+        if (value != null) {
+            switch (cmd) {
+                case VOLUME_SET:
+                    messageStr += String.format("%02d", value);
+                    break;
+                case BASS_SET:
+                case TREBLE_SET:
+                    if (value == 0) {
+                        messageStr += "000";
+                    } else if (value > 0) {
+                        messageStr += String.format("+%02d", value);
+                    } else {
+                        messageStr += String.format("-%02d", -value);
+                    }
+                    break;
+                case BALANCE_SET:
+                    if (value == 0) {
+                        messageStr += "000";
+                    } else if (value > 0) {
+                        messageStr += String.format("r%02d", value);
+                    } else {
+                        messageStr += String.format("l%02d", -value);
+                    }
+                    break;
+                case DIMMER_LEVEL_SET:
+                    if (value > 0 && model.getDimmerLevelMin() < 0) {
+                        messageStr += String.format("+%d", value);
+                    } else {
+                        messageStr += String.format("%d", value);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+        if (!messageStr.endsWith("?")) {
+            messageStr += "!";
+        }
+        byte[] message = messageStr.getBytes(StandardCharsets.US_ASCII);
+        logger.debug("Command \"{}\" => {}", cmd.getName(), messageStr);
+        return message;
+    }
+}
diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/hex/RotelHexProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/hex/RotelHexProtocolHandler.java
new file mode 100644 (file)
index 0000000..d372033
--- /dev/null
@@ -0,0 +1,778 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.rotel.internal.protocol.hex;
+
+import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.rotel.internal.RotelException;
+import org.openhab.binding.rotel.internal.RotelModel;
+import org.openhab.binding.rotel.internal.communication.RotelCommand;
+import org.openhab.binding.rotel.internal.communication.RotelDsp;
+import org.openhab.binding.rotel.internal.communication.RotelFlagsMapping;
+import org.openhab.binding.rotel.internal.communication.RotelSource;
+import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
+import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for handling the Rotel HEX protocol (build of command messages, decoding of incoming data)
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class RotelHexProtocolHandler extends RotelAbstractProtocolHandler {
+
+    public static final byte START = (byte) 0xFE;
+
+    private static final String KEY1_HEX_VOLUME = "volume ";
+    private static final String KEY2_HEX_VOLUME = "vol ";
+    private static final String KEY_HEX_MUTE = "mute ";
+    private static final String KEY1_HEX_BASS = "bass ";
+    private static final String KEY2_HEX_BASS = "lf ";
+    private static final String KEY1_HEX_TREBLE = "treble ";
+    private static final String KEY2_HEX_TREBLE = "hf ";
+    private static final String KEY_HEX_MULTI_IN = "multi in ";
+    private static final String KEY_HEX_STEREO = "stereo";
+    private static final String KEY1_HEX_3CH = "3 stereo";
+    private static final String KEY2_HEX_3CH = "dolby 3 stereo";
+    private static final String KEY_HEX_5CH = "5ch stereo";
+    private static final String KEY_HEX_7CH = "7ch stereo";
+    private static final String KEY_HEX_MUSIC1 = "music 1";
+    private static final String KEY_HEX_MUSIC2 = "music 2";
+    private static final String KEY_HEX_MUSIC3 = "music 3";
+    private static final String KEY_HEX_MUSIC4 = "music 4";
+    private static final String KEY_HEX_DSP1 = "dsp 1";
+    private static final String KEY_HEX_DSP2 = "dsp 2";
+    private static final String KEY_HEX_DSP3 = "dsp 3";
+    private static final String KEY_HEX_DSP4 = "dsp 4";
+    private static final String KEY1_HEX_PROLOGIC = "prologic  emu";
+    private static final String KEY2_HEX_PROLOGIC = "dolby pro logic";
+    private static final String KEY1_HEX_PLII_CINEMA = "prologic  cin";
+    private static final String KEY2_HEX_PLII_CINEMA = "dolby pl  c";
+    private static final String KEY1_HEX_PLII_MUSIC = "prologic  mus";
+    private static final String KEY2_HEX_PLII_MUSIC = "dolby pl  m";
+    private static final String KEY1_HEX_PLII_GAME = "prologic  gam";
+    private static final String KEY2_HEX_PLII_GAME = "dolby pl  g";
+    private static final String KEY1_HEX_PLIIX_CINEMA = "pl x cinema";
+    private static final String KEY2_HEX_PLIIX_CINEMA = "dolby pl x c";
+    private static final String KEY1_HEX_PLIIX_MUSIC = "pl x music";
+    private static final String KEY2_HEX_PLIIX_MUSIC = "dolby pl x m";
+    private static final String KEY1_HEX_PLIIX_GAME = "pl x game";
+    private static final String KEY2_HEX_PLIIX_GAME = "dolby pl x g";
+    private static final String KEY_HEX_PLIIZ = "dolby pl z";
+    private static final String KEY1_HEX_DTS_NEO6_CINEMA = "neo 6 cinema";
+    private static final String KEY2_HEX_DTS_NEO6_CINEMA = "dts neo:6 c";
+    private static final String KEY1_HEX_DTS_NEO6_MUSIC = "neo 6 music";
+    private static final String KEY2_HEX_DTS_NEO6_MUSIC = "dts neo:6 m";
+    private static final String KEY_HEX_DTS = "dts";
+    private static final String KEY_HEX_DTS_ES = "dts-es";
+    private static final String KEY_HEX_DTS_96 = "dts 96";
+    private static final String KEY_HEX_DD = "dolby digital";
+    private static final String KEY_HEX_DD_EX = "dolby d ex";
+    private static final String KEY_HEX_PCM = "pcm";
+    private static final String KEY_HEX_LPCM = "lpcm";
+    private static final String KEY_HEX_MPEG = "mpeg";
+    private static final String KEY_HEX_BYPASS = "bypass";
+    private static final String KEY1_HEX_ZONE2 = "zone ";
+    private static final String KEY2_HEX_ZONE2 = "zone2 ";
+    private static final String KEY_HEX_ZONE3 = "zone3 ";
+    private static final String KEY_HEX_ZONE4 = "zone4 ";
+    private static final String KEY_HEX_RECORD = "rec ";
+    private static final String SOURCE = "source";
+
+    private final Logger logger = LoggerFactory.getLogger(RotelHexProtocolHandler.class);
+
+    private final Map<RotelSource, String> sourcesLabels;
+
+    private final int size;
+    private final byte[] dataBuffer;
+
+    private boolean startCodeReached;
+    private int count;
+    private int index;
+
+    /**
+     * Constructor
+     *
+     * @param model the Rotel model in use
+     * @param sourcesLabels the custom labels for sources
+     */
+    public RotelHexProtocolHandler(RotelModel model, Map<RotelSource, String> sourcesLabels) {
+        super(model);
+        this.sourcesLabels = sourcesLabels;
+        this.size = (6 + model.getRespNbChars() + model.getRespNbFlags());
+        this.dataBuffer = new byte[size];
+        this.startCodeReached = false;
+        this.count = 0;
+        this.index = 0;
+    }
+
+    @Override
+    public RotelProtocol getProtocol() {
+        return RotelProtocol.HEX;
+    }
+
+    @Override
+    public byte[] buildCommandMessage(RotelCommand cmd, @Nullable Integer value) throws RotelException {
+        if (cmd.getHexType() == 0) {
+            throw new RotelException("Command \"" + cmd.getName() + "\" ignored: not available for HEX protocol");
+        }
+        final int size = 6;
+        byte[] message = new byte[size];
+        int idx = 0;
+        message[idx++] = START;
+        message[idx++] = 3;
+        message[idx++] = model.getDeviceId();
+        message[idx++] = cmd.getHexType();
+        message[idx++] = (value == null) ? cmd.getHexKey() : (byte) (value & 0x000000FF);
+        final byte checksum = computeCheckSum(message, idx - 1);
+        if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
+            message = Arrays.copyOf(message, size + 1);
+            message[idx++] = (byte) 0xFD;
+            message[idx++] = ((checksum & 0x000000FF) == 0x000000FD) ? (byte) 0 : (byte) 1;
+        } else {
+            message[idx++] = checksum;
+        }
+        logger.debug("Command \"{}\" => {}", cmd.getName(), HexUtils.bytesToHex(message));
+        return message;
+    }
+
+    @Override
+    public void handleIncomingData(byte[] inDataBuffer, int length) {
+        for (int i = 0; i < length; i++) {
+            if (inDataBuffer[i] == RotelHexProtocolHandler.START) {
+                startCodeReached = true;
+                count = 0;
+                index = 0;
+            }
+            if (startCodeReached) {
+                if (index < size) {
+                    dataBuffer[index++] = inDataBuffer[i];
+                }
+                if (index == 2) {
+                    count = inDataBuffer[i];
+                } else if ((count > 0) && (index == (count + 3))) {
+                    if ((inDataBuffer[i] & 0x000000FF) == 0x000000FD) {
+                        count++;
+                    } else {
+                        byte[] msg = Arrays.copyOf(dataBuffer, index);
+                        handleIncomingMessage(msg);
+                        startCodeReached = false;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Validate the content of a feedback message
+     *
+     * @param responseMessage the buffer containing the feedback message
+     *
+     * @throws RotelException - If the message has unexpected content
+     */
+    @Override
+    protected void validateResponse(byte[] responseMessage) throws RotelException {
+        // Check minimum message length
+        if (responseMessage.length < 6) {
+            logger.debug("Unexpected message length: {}", responseMessage.length);
+            throw new RotelException("Unexpected message length");
+        }
+
+        // Check START
+        if (responseMessage[0] != START) {
+            logger.debug("Unexpected START in response: {} rather than {}",
+                    Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
+            throw new RotelException("Unexpected START in response");
+        }
+
+        // Check ID
+        if (responseMessage[2] != model.getDeviceId()) {
+            logger.debug("Unexpected ID in response: {} rather than {}",
+                    Integer.toHexString(responseMessage[2] & 0x000000FF),
+                    Integer.toHexString(model.getDeviceId() & 0x000000FF));
+            throw new RotelException("Unexpected ID in response");
+        }
+
+        // Check TYPE
+        if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
+                && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
+                && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
+                && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
+                && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
+                && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
+                && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
+                && responseMessage[3] != TRIGGER_CMD) {
+            logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
+            throw new RotelException("Unexpected TYPE in response");
+        }
+
+        int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
+                ? (5 + model.getRespNbChars() + model.getRespNbFlags())
+                : responseMessage.length;
+
+        // Check COUNT
+        if (responseMessage[1] != (expectedLen - 3)) {
+            logger.debug("Unexpected COUNT in response: {} rather than {}",
+                    Integer.toHexString(responseMessage[1] & 0x000000FF),
+                    Integer.toHexString((expectedLen - 3) & 0x000000FF));
+            throw new RotelException("Unexpected COUNT in response");
+        }
+
+        final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
+        if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
+            expectedLen++;
+        }
+
+        // Check message length
+        if (responseMessage.length != expectedLen) {
+            logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
+            throw new RotelException("Unexpected message length");
+        }
+
+        // Check sum
+        if ((checksum & 0x000000FF) == 0x000000FD) {
+            if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
+                    || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
+                logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
+                        Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
+                throw new RotelException("Invalid check sum in response");
+            }
+        } else if ((checksum & 0x000000FF) == 0x000000FE) {
+            if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
+                    || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
+                logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
+                        Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
+                throw new RotelException("Invalid check sum in response");
+            }
+        } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
+            logger.debug("Invalid check sum in response: {} rather than {}",
+                    Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
+                    Integer.toHexString(checksum & 0x000000FF));
+            throw new RotelException("Invalid check sum in response");
+        }
+    }
+
+    /**
+     * Compute the checksum of a message
+     *
+     * @param message the buffer containing the message
+     * @param maxIdx the position in the buffer at which the sum has to be stopped
+     *
+     * @return the checksum as a byte
+     */
+    public static byte computeCheckSum(byte[] message, int maxIdx) {
+        int result = 0;
+        for (int i = 1; i <= maxIdx; i++) {
+            result += (message[i] & 0x000000FF);
+        }
+        return (byte) (result & 0x000000FF);
+    }
+
+    /**
+     * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
+     *
+     * @param incomingMessage the received message
+     */
+    @Override
+    protected void handleValidMessage(byte[] incomingMessage) {
+        if (incomingMessage[3] != STANDARD_RESPONSE) {
+            return;
+        }
+
+        final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
+
+        // Replace characters with code < 32 by a space before converting to a string
+        for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
+            if (incomingMessage[i] < 0x20) {
+                incomingMessage[i] = 0x20;
+            }
+        }
+
+        String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
+        logger.debug("handleValidHexMessage: chars *{}*", value);
+
+        final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
+        final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
+        if (logger.isTraceEnabled()) {
+            for (int i = 1; i <= flags.length; i++) {
+                try {
+                    logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
+                            Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
+                            RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
+                            RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
+                            RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
+                            RotelFlagsMapping.isBitFlagOn(flags, i, 0));
+                } catch (RotelException e1) {
+                }
+            }
+        }
+        try {
+            dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
+        } catch (RotelException e1) {
+            // Can't get zone power information from flags data, so we just do not notify of this information that way
+        }
+        try {
+            dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
+        } catch (RotelException e1) {
+            // Can't get zone power information from flags data, so we just do not notify of this information that way
+        }
+        try {
+            dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
+        } catch (RotelException e1) {
+            // Can't get zone power information from flags data, so we just do not notify of this information that way
+        }
+        boolean checkMultiIn = false;
+        boolean checkSource = true;
+        try {
+            if (model.isMultiInputOn(flags)) {
+                checkSource = false;
+                try {
+                    RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
+                    RotelCommand cmd = source.getCommand();
+                    if (cmd != null) {
+                        String value2 = cmd.getAsciiCommandV2();
+                        if (value2 != null) {
+                            dispatchKeyValue(KEY_SOURCE, value2);
+                        }
+                    }
+                } catch (RotelException e1) {
+                    // MULTI source not declared for the model (should not happen), we do not notify of this source
+                }
+            }
+        } catch (RotelException e1) {
+            // Can't get status of multiple input source from flags data, checkMultiIn is set to true to get this
+            // information in another way
+            checkMultiIn = true;
+        }
+        boolean checkStereo = true;
+        try {
+            checkStereo = !model.isMoreThan2Channels(flags);
+        } catch (RotelException e1) {
+            // Can't get stereo information from flags data, checkStereo is set to true to get this information in
+            // another way
+        }
+
+        String valueLowerCase = value.trim().toLowerCase();
+        if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
+                && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
+                && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
+            dispatchKeyValue(KEY_POWER, POWER_ON);
+        }
+
+        if (model.getRespNbChars() == 42) {
+            // 2 lines of 21 characters with a left part and a right part
+
+            // Line 1 left
+            value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
+            logger.debug("handleValidHexMessage: line 1 left *{}*", value);
+            parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
+
+            // Line 1 right
+            value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
+            logger.debug("handleValidHexMessage: line 1 right *{}*", value);
+            parseText(value, false, false, false, false, false, false, false, true);
+
+            // Full line 1
+            value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
+            dispatchKeyValue(KEY_LINE1, value);
+
+            // Line 2 right
+            value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
+            logger.debug("handleValidHexMessage: line 2 right *{}*", value);
+            parseText(value, false, false, false, false, false, false, false, true);
+
+            // Full line 2
+            value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
+            logger.debug("handleValidHexMessage: line 2 *{}*", value);
+            parseText(value, false, false, true, true, false, true, true, true);
+            dispatchKeyValue(KEY_LINE2, value);
+        } else {
+            value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
+            parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
+            dispatchKeyValue(KEY_LINE1, value);
+        }
+
+        if (valueLowerCase.isEmpty()) {
+            dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
+        }
+    }
+
+    /**
+     * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
+     *
+     * @param text the text to be parsed
+     * @param searchSource true if a source has to be searched in the text
+     * @param searchMultiIn true if MULTI IN indication has to be searched in the text
+     * @param searchZone true if a zone information has to be searched in the text
+     * @param searchRecord true if a record source has to be searched in the text
+     * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
+     * @param searchDsp true if a DSP mode has to be searched in the text
+     * @param searchStereo true if a STEREO has to be considered in the search
+     * @param multipleInfo true if source and volume/mute are provided separately
+     */
+    private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
+            boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
+            boolean multipleInfo) {
+        String value = text.trim();
+        String valueLowerCase = value.toLowerCase();
+        if (searchRecord) {
+            dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
+        }
+        if (searchZone) {
+            if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
+                dispatchKeyValue(KEY_ZONE, "2");
+            } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
+                dispatchKeyValue(KEY_ZONE, "3");
+            } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
+                dispatchKeyValue(KEY_ZONE, "4");
+            } else {
+                dispatchKeyValue(KEY_ZONE, "1");
+            }
+        }
+        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
+            value = extractNumber(value,
+                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
+            dispatchKeyValue(KEY_VOLUME, value);
+            dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
+        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
+            value = value.substring(KEY_HEX_MUTE.length()).trim();
+            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
+            } else {
+                logger.debug("Invalid value {} for zone mute", value);
+            }
+        } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
+            value = extractNumber(value,
+                    valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
+            dispatchKeyValue(KEY_BASS, value);
+        } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
+            value = extractNumber(value,
+                    valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
+            dispatchKeyValue(KEY_TREBLE, value);
+        } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
+            value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
+            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                try {
+                    RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
+                    RotelCommand cmd = source.getCommand();
+                    if (cmd != null) {
+                        String value2 = cmd.getAsciiCommandV2();
+                        if (value2 != null) {
+                            dispatchKeyValue(KEY_SOURCE, value2);
+                        }
+                    }
+                } catch (RotelException e1) {
+                    // MULTI source not declared for the model (should not happen), we do not notify of this source
+                }
+            } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+                logger.debug("Invalid value {} for MULTI IN", value);
+            }
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_BYPASS.getFeedback());
+        } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
+        } else if (searchDsp
+                && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
+        } else if (searchDsp
+                && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
+        } else if (searchDsp
+                && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
+        } else if (searchDsp
+                && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
+        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
+                || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
+                || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
+        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
+                || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
+                || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
+        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
+                || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
+                || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
+        } else if (searchDsp
+                && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
+        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
+                || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
+        } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
+                || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
+            logger.debug("DTS-ES");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
+            logger.debug("DTS 96");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
+            logger.debug("DTS");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
+            logger.debug("DD-EX");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
+            logger.debug("DD");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
+            logger.debug("LPCM");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
+            logger.debug("PCM");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
+            logger.debug("MPEG");
+            dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
+        } else if (searchZone
+                && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
+            value = value.substring(
+                    valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
+            parseZone2(value, multipleInfo);
+        } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
+            parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
+        } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
+            parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
+        } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
+            parseRecord(value.substring(KEY_HEX_RECORD.length()));
+        } else if (searchSource || searchRecordAfterSource) {
+            parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
+        }
+    }
+
+    /**
+     * Parse a text to identify a source
+     *
+     * @param text the text to be parsed
+     * @param acceptFollowMain true if follow main has to be considered in the search
+     *
+     * @return the identified source or null if no source is identified in the text
+     */
+    private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
+        String value = text.trim();
+        RotelSource source = null;
+        if (!value.isEmpty()) {
+            if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
+                try {
+                    source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
+                } catch (RotelException e) {
+                    // MAIN (follow main zone source) source not declared for the model, we return null
+                }
+            } else {
+                for (RotelSource src : sourcesLabels.keySet()) {
+                    String label = sourcesLabels.get(src);
+                    if (label != null && value.startsWith(label)) {
+                        if (source == null || sourcesLabels.get(source).length() < label.length()) {
+                            source = src;
+                        }
+                    }
+                }
+            }
+        }
+        return source;
+    }
+
+    private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
+            boolean multipleInfo) {
+        RotelSource source = parseSource(text, false);
+        if (source != null) {
+            if (searchSource) {
+                RotelCommand cmd = source.getCommand();
+                if (cmd != null) {
+                    String value2 = cmd.getAsciiCommandV2();
+                    if (value2 != null) {
+                        dispatchKeyValue(KEY_SOURCE, value2);
+                        if (!multipleInfo) {
+                            dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
+                        }
+                    }
+                }
+            }
+
+            if (searchRecordAfterSource) {
+                String value = text.substring(getSourceLabel(source).length()).trim();
+                source = parseSource(value, true);
+                if (source != null) {
+                    RotelCommand cmd = source.getRecordCommand();
+                    if (cmd != null) {
+                        value = cmd.getAsciiCommandV2();
+                        if (value != null) {
+                            dispatchKeyValue(KEY_RECORD, value);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private String getSourceLabel(RotelSource source) {
+        String label = sourcesLabels.get(source);
+        return (label == null) ? source.getLabel() : label;
+    }
+
+    private void parseRecord(String text) {
+        String value = text.trim();
+        RotelSource source = parseSource(value, true);
+        if (source != null) {
+            RotelCommand cmd = source.getRecordCommand();
+            if (cmd != null) {
+                value = cmd.getAsciiCommandV2();
+                if (value != null) {
+                    dispatchKeyValue(KEY_RECORD, value);
+                }
+            }
+        } else {
+            logger.debug("Invalid value {} for record source", value);
+        }
+    }
+
+    private void parseZone2(String text, boolean multipleInfo) {
+        String value = text.trim();
+        String valueLowerCase = value.toLowerCase();
+        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
+            value = extractNumber(value,
+                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
+            dispatchKeyValue(KEY_VOLUME_ZONE2, value);
+            dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
+        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
+            value = value.substring(KEY_HEX_MUTE.length()).trim();
+            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
+            } else {
+                logger.debug("Invalid value {} for zone mute", value);
+            }
+        } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+            RotelSource source = parseSource(value, true);
+            if (source != null) {
+                RotelCommand cmd = source.getZone2Command();
+                if (cmd != null) {
+                    value = cmd.getAsciiCommandV2();
+                    if (value != null) {
+                        dispatchKeyValue(KEY_SOURCE_ZONE2, value);
+                        if (!multipleInfo) {
+                            dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
+                        }
+                    }
+                }
+            } else {
+                logger.debug("Invalid value {} for zone 2 source", value);
+            }
+        }
+    }
+
+    private void parseZone3(String text, boolean multipleInfo) {
+        String value = text.trim();
+        String valueLowerCase = value.toLowerCase();
+        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
+            value = extractNumber(value,
+                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
+            dispatchKeyValue(KEY_VOLUME_ZONE3, value);
+            dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
+        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
+            value = value.substring(KEY_HEX_MUTE.length()).trim();
+            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
+            } else {
+                logger.debug("Invalid value {} for zone mute", value);
+            }
+        } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+            RotelSource source = parseSource(value, true);
+            if (source != null) {
+                RotelCommand cmd = source.getZone3Command();
+                if (cmd != null) {
+                    value = cmd.getAsciiCommandV2();
+                    if (value != null) {
+                        dispatchKeyValue(KEY_SOURCE_ZONE3, value);
+                        if (!multipleInfo) {
+                            dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
+                        }
+                    }
+                }
+            } else {
+                logger.debug("Invalid value {} for zone 3 source", value);
+            }
+        }
+    }
+
+    private void parseZone4(String text, boolean multipleInfo) {
+        String value = text.trim();
+        String valueLowerCase = value.toLowerCase();
+        if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
+            value = extractNumber(value,
+                    valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
+            dispatchKeyValue(KEY_VOLUME_ZONE4, value);
+            dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
+        } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
+            value = value.substring(KEY_HEX_MUTE.length()).trim();
+            if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
+                dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
+            } else {
+                logger.debug("Invalid value {} for zone mute", value);
+            }
+        } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
+            RotelSource source = parseSource(value, true);
+            if (source != null) {
+                RotelCommand cmd = source.getZone4Command();
+                if (cmd != null) {
+                    value = cmd.getAsciiCommandV2();
+                    if (value != null) {
+                        dispatchKeyValue(KEY_SOURCE_ZONE4, value);
+                        if (!multipleInfo) {
+                            dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
+                        }
+                    }
+                }
+            } else {
+                logger.debug("Invalid value {} for zone 4 source", value);
+            }
+        }
+    }
+
+    /**
+     * Extract from a string a number
+     *
+     * @param value the string
+     * @param startIndex the index in the string at which the integer has to be extracted
+     *
+     * @return the number as a string with its sign and no blank between the sign and the digits
+     */
+    private String extractNumber(String value, int startIndex) {
+        String result = value.substring(startIndex).trim();
+        // Delete possible blank(s) between the sign and the number
+        if (result.startsWith("+") || result.startsWith("-")) {
+            result = result.substring(0, 1) + result.substring(1, result.length()).trim();
+        }
+        return result;
+    }
+}