]> git.basschouten.com Git - openhab-addons.git/commitdiff
[rotel] Add support of multiple zones (ASCII protocol) (#13136)
authorlolodomo <lg.hc@free.fr>
Sat, 23 Jul 2022 11:41:38 +0000 (13:41 +0200)
committerGitHub <noreply@github.com>
Sat, 23 Jul 2022 11:41:38 +0000 (13:41 +0200)
* [rotel] Add support of multiple zones (ASCII protocol)

Models C8 and C8+ (distribution amplifiers)

Code factorization.
Simulator updated.

* Set model/firmware thing properties (ASCII V2 protocol)
* Review comment: NumberOf rather than Nb
* Review comment: getZoneCommand: IllegalArgumentException if numZone is
outside 1-4
* Review comment: constant for volumeUpDown
* Simplification: method isPowerOn with numZone parameter (range 0-4)
* Review comment: fix for getPowerOffCommand
* Review comment: new method isZoneAvailable to factorize code
* Review comment: use MAX_NUMBER_OF_ZONES to check range validity

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
12 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/RotelHandlerFactory.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/RotelSimuConnector.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSource.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/ascii/RotelAsciiV2ProtocolHandler.java
bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/hex/RotelHexProtocolHandler.java
bundles/org.openhab.binding.rotel/src/main/resources/OH-INF/i18n/rotel.properties
bundles/org.openhab.binding.rotel/src/main/resources/OH-INF/thing/c8.xml [new file with mode: 0644]
bundles/org.openhab.binding.rotel/src/main/resources/OH-INF/thing/channels.xml

index 31319938131411d57c3816da64be7923faebf80a..8f2169fbe2801aca906b151edc32078d6e7de7eb 100644 (file)
@@ -68,6 +68,7 @@ public class RotelBindingConstants {
     public static final String THING_TYPE_ID_RT1570 = "rt1570";
     public static final String THING_TYPE_ID_T11 = "t11";
     public static final String THING_TYPE_ID_T14 = "t14";
+    public static final String THING_TYPE_ID_C8 = "c8";
     public static final String THING_TYPE_ID_M8 = "m8";
     public static final String THING_TYPE_ID_P5 = "p5";
     public static final String THING_TYPE_ID_S5 = "s5";
@@ -116,6 +117,7 @@ public class RotelBindingConstants {
     public static final ThingTypeUID THING_TYPE_RT1570 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_RT1570);
     public static final ThingTypeUID THING_TYPE_T11 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_T11);
     public static final ThingTypeUID THING_TYPE_T14 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_T14);
+    public static final ThingTypeUID THING_TYPE_C8 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_C8);
     public static final ThingTypeUID THING_TYPE_M8 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_M8);
     public static final ThingTypeUID THING_TYPE_P5 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_P5);
     public static final ThingTypeUID THING_TYPE_S5 = new ThingTypeUID(BINDING_ID, THING_TYPE_ID_S5);
@@ -124,45 +126,80 @@ public class RotelBindingConstants {
 
     // List of all Channel ids
     public static final String CHANNEL_POWER = "power";
-    public static final String CHANNEL_MAIN_POWER = "mainZone#power";
     public static final String CHANNEL_SOURCE = "source";
-    public static final String CHANNEL_MAIN_SOURCE = "mainZone#source";
-    public static final String CHANNEL_MAIN_RECORD_SOURCE = "mainZone#recordSource";
+    public static final String CHANNEL_RECORD_SOURCE = "recordSource";
     public static final String CHANNEL_DSP = "dsp";
-    public static final String CHANNEL_MAIN_DSP = "mainZone#dsp";
     public static final String CHANNEL_VOLUME = "volume";
-    public static final String CHANNEL_MAIN_VOLUME = "mainZone#volume";
-    public static final String CHANNEL_MAIN_VOLUME_UP_DOWN = "mainZone#volumeUpDown";
+    public static final String CHANNEL_VOLUME_UP_DOWN = "volumeUpDown";
     public static final String CHANNEL_MUTE = "mute";
-    public static final String CHANNEL_MAIN_MUTE = "mainZone#mute";
     public static final String CHANNEL_BASS = "bass";
-    public static final String CHANNEL_MAIN_BASS = "mainZone#bass";
     public static final String CHANNEL_TREBLE = "treble";
-    public static final String CHANNEL_MAIN_TREBLE = "mainZone#treble";
     public static final String CHANNEL_PLAY_CONTROL = "playControl";
     public static final String CHANNEL_TRACK = "track";
     public static final String CHANNEL_FREQUENCY = "frequency";
     public static final String CHANNEL_LINE1 = "mainZone#line1";
     public static final String CHANNEL_LINE2 = "mainZone#line2";
     public static final String CHANNEL_BRIGHTNESS = "brightness";
-    public static final String CHANNEL_ZONE2_POWER = "zone2#power";
-    public static final String CHANNEL_ZONE2_SOURCE = "zone2#source";
-    public static final String CHANNEL_ZONE2_VOLUME = "zone2#volume";
-    public static final String CHANNEL_ZONE2_VOLUME_UP_DOWN = "zone2#volumeUpDown";
-    public static final String CHANNEL_ZONE2_MUTE = "zone2#mute";
-    public static final String CHANNEL_ZONE3_POWER = "zone3#power";
-    public static final String CHANNEL_ZONE3_SOURCE = "zone3#source";
-    public static final String CHANNEL_ZONE3_VOLUME = "zone3#volume";
-    public static final String CHANNEL_ZONE3_MUTE = "zone3#mute";
-    public static final String CHANNEL_ZONE4_POWER = "zone4#power";
-    public static final String CHANNEL_ZONE4_SOURCE = "zone4#source";
-    public static final String CHANNEL_ZONE4_VOLUME = "zone4#volume";
-    public static final String CHANNEL_ZONE4_MUTE = "zone4#mute";
     public static final String CHANNEL_TCBYPASS = "tcbypass";
     public static final String CHANNEL_BALANCE = "balance";
     public static final String CHANNEL_SPEAKER_A = "speakera";
     public static final String CHANNEL_SPEAKER_B = "speakerb";
 
+    public static final String CHANNEL_GROUP_ALL_ZONES = "allZones";
+    public static final String CHANNEL_ALL_POWER = CHANNEL_GROUP_ALL_ZONES + "#" + CHANNEL_POWER;
+    public static final String CHANNEL_ALL_BRIGHTNESS = CHANNEL_GROUP_ALL_ZONES + "#" + CHANNEL_BRIGHTNESS;
+
+    public static final String CHANNEL_GROUP_MAIN_ZONE = "mainZone";
+    public static final String CHANNEL_MAIN_POWER = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_POWER;
+    public static final String CHANNEL_MAIN_SOURCE = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_SOURCE;
+    public static final String CHANNEL_MAIN_RECORD_SOURCE = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_RECORD_SOURCE;
+    public static final String CHANNEL_MAIN_DSP = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_DSP;
+    public static final String CHANNEL_MAIN_VOLUME = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_VOLUME;
+    public static final String CHANNEL_MAIN_VOLUME_UP_DOWN = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_VOLUME_UP_DOWN;
+    public static final String CHANNEL_MAIN_MUTE = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_MUTE;
+    public static final String CHANNEL_MAIN_BASS = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_BASS;
+    public static final String CHANNEL_MAIN_TREBLE = CHANNEL_GROUP_MAIN_ZONE + "#" + CHANNEL_TREBLE;
+
+    public static final String CHANNEL_GROUP_ZONE1 = "zone1";
+    public static final String CHANNEL_ZONE1_SOURCE = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_SOURCE;
+    public static final String CHANNEL_ZONE1_VOLUME = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_VOLUME;
+    public static final String CHANNEL_ZONE1_MUTE = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_MUTE;
+    public static final String CHANNEL_ZONE1_BASS = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_BASS;
+    public static final String CHANNEL_ZONE1_TREBLE = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_TREBLE;
+    public static final String CHANNEL_ZONE1_BALANCE = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_BALANCE;
+    public static final String CHANNEL_ZONE1_FREQUENCY = CHANNEL_GROUP_ZONE1 + "#" + CHANNEL_FREQUENCY;
+
+    public static final String CHANNEL_GROUP_ZONE2 = "zone2";
+    public static final String CHANNEL_ZONE2_POWER = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_POWER;
+    public static final String CHANNEL_ZONE2_SOURCE = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_SOURCE;
+    public static final String CHANNEL_ZONE2_VOLUME = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_VOLUME;
+    public static final String CHANNEL_ZONE2_VOLUME_UP_DOWN = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_VOLUME_UP_DOWN;
+    public static final String CHANNEL_ZONE2_MUTE = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_MUTE;
+    public static final String CHANNEL_ZONE2_BASS = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_BASS;
+    public static final String CHANNEL_ZONE2_TREBLE = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_TREBLE;
+    public static final String CHANNEL_ZONE2_BALANCE = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_BALANCE;
+    public static final String CHANNEL_ZONE2_FREQUENCY = CHANNEL_GROUP_ZONE2 + "#" + CHANNEL_FREQUENCY;
+
+    public static final String CHANNEL_GROUP_ZONE3 = "zone3";
+    public static final String CHANNEL_ZONE3_POWER = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_POWER;
+    public static final String CHANNEL_ZONE3_SOURCE = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_SOURCE;
+    public static final String CHANNEL_ZONE3_VOLUME = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_VOLUME;
+    public static final String CHANNEL_ZONE3_MUTE = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_MUTE;
+    public static final String CHANNEL_ZONE3_BASS = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_BASS;
+    public static final String CHANNEL_ZONE3_TREBLE = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_TREBLE;
+    public static final String CHANNEL_ZONE3_BALANCE = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_BALANCE;
+    public static final String CHANNEL_ZONE3_FREQUENCY = CHANNEL_GROUP_ZONE3 + "#" + CHANNEL_FREQUENCY;
+
+    public static final String CHANNEL_GROUP_ZONE4 = "zone4";
+    public static final String CHANNEL_ZONE4_POWER = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_POWER;
+    public static final String CHANNEL_ZONE4_SOURCE = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_SOURCE;
+    public static final String CHANNEL_ZONE4_VOLUME = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_VOLUME;
+    public static final String CHANNEL_ZONE4_MUTE = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_MUTE;
+    public static final String CHANNEL_ZONE4_BASS = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_BASS;
+    public static final String CHANNEL_ZONE4_TREBLE = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_TREBLE;
+    public static final String CHANNEL_ZONE4_BALANCE = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_BALANCE;
+    public static final String CHANNEL_ZONE4_FREQUENCY = CHANNEL_GROUP_ZONE4 + "#" + CHANNEL_FREQUENCY;
+
     // List of all properties
     public static final String PROPERTY_PROTOCOL = "protocol";
 
@@ -186,13 +223,34 @@ public class RotelBindingConstants {
     // 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_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 = "mute";
+    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_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_INPUT = "input";
+    public static final String KEY_INPUT_ZONE1 = "input_zone1";
+    public static final String KEY_INPUT_ZONE2 = "input_zone2";
+    public static final String KEY_INPUT_ZONE3 = "input_zone3";
+    public static final String KEY_INPUT_ZONE4 = "input_zone4";
+    public static final String KEY_VOLUME_ZONE1 = "volume_zone1";
+    public static final String KEY_MUTE_ZONE1 = "mute_zone1";
+    public static final String KEY_BASS_ZONE1 = "bass_zone1";
+    public static final String KEY_BASS_ZONE2 = "bass_zone2";
+    public static final String KEY_BASS_ZONE3 = "bass_zone3";
+    public static final String KEY_BASS_ZONE4 = "bass_zone4";
+    public static final String KEY_TREBLE_ZONE1 = "treble_zone1";
+    public static final String KEY_TREBLE_ZONE2 = "treble_zone2";
+    public static final String KEY_TREBLE_ZONE3 = "treble_zone3";
+    public static final String KEY_TREBLE_ZONE4 = "treble_zone4";
     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";
@@ -203,10 +261,20 @@ public class RotelBindingConstants {
     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_FREQ_ZONE1 = "freq_zone1";
+    public static final String KEY_FREQ_ZONE2 = "freq_zone2";
+    public static final String KEY_FREQ_ZONE3 = "freq_zone3";
+    public static final String KEY_FREQ_ZONE4 = "freq_zone4";
     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_BALANCE_ZONE1 = "balance_zone1";
+    public static final String KEY_BALANCE_ZONE2 = "balance_zone2";
+    public static final String KEY_BALANCE_ZONE3 = "balance_zone3";
+    public static final String KEY_BALANCE_ZONE4 = "balance_zone4";
     public static final String KEY_SPEAKER = "speaker";
+    public static final String KEY_MODEL = "model";
+    public static final String KEY_VERSION = "version";
     // Output keys only used by the HEX protocol
     public static final String KEY_LINE1 = "line1";
     public static final String KEY_LINE2 = "line2";
@@ -219,16 +287,11 @@ public class RotelBindingConstants {
     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 MSG_VALUE_NONE = "none";
     public static final String POWER_ON = "on";
     public static final String STANDBY = "standby";
     public static final String POWER_OFF_DELAYED = "off_delayed";
@@ -243,4 +306,6 @@ public class RotelBindingConstants {
     public static final String PLAY = "play";
     public static final String PAUSE = "pause";
     public static final String STOP = "stop";
+
+    public static final int MAX_NUMBER_OF_ZONES = 4;
 }
index 56b7ecd1fe0f5a086d4d26c9260328001eed36a8..f97da711b6af2b204e0862eab40115b2b537e77f 100644 (file)
@@ -50,8 +50,8 @@ public class RotelHandlerFactory extends BaseThingHandlerFactory {
                     THING_TYPE_RA12, THING_TYPE_RA1570, THING_TYPE_RA1572, THING_TYPE_RA1592, THING_TYPE_RAP1580,
                     THING_TYPE_RC1570, THING_TYPE_RC1572, THING_TYPE_RC1590, THING_TYPE_RCD1570, THING_TYPE_RCD1572,
                     THING_TYPE_RCX1500, THING_TYPE_RDD1580, THING_TYPE_RDG1520, THING_TYPE_RSP1576, THING_TYPE_RSP1582,
-                    THING_TYPE_RT09, THING_TYPE_RT11, THING_TYPE_RT1570, THING_TYPE_T11, THING_TYPE_T14, THING_TYPE_M8,
-                    THING_TYPE_P5, THING_TYPE_S5, THING_TYPE_X3, THING_TYPE_X5)
+                    THING_TYPE_RT09, THING_TYPE_RT11, THING_TYPE_RT1570, THING_TYPE_T11, THING_TYPE_T14, THING_TYPE_C8,
+                    THING_TYPE_M8, THING_TYPE_P5, THING_TYPE_S5, THING_TYPE_X3, THING_TYPE_X5)
             .collect(Collectors.toSet()));
 
     private final SerialPortManager serialPortManager;
index 38bf2063d11cbfd50c29499755aeb1c788532698..d1656b3cb34ed0908cafe2846aa51f2e9e009874 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.rotel.internal;
 
+import static org.openhab.binding.rotel.internal.RotelBindingConstants.MAX_NUMBER_OF_ZONES;
 import static org.openhab.binding.rotel.internal.communication.RotelCommand.*;
 import static org.openhab.binding.rotel.internal.protocol.ascii.RotelAbstractAsciiProtocolHandler.*;
 
@@ -89,6 +90,8 @@ public enum RotelModel {
     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),
+    C8("C8", 115200, POWER, 21, 3, true, false, 96, true, 10, false, 10, false, null, -1, true, false, true, 4, 0,
+            (byte) 0, 0, 0, false, RotelFlagsMapping.NO_MAPPING, NO_SPECIAL_CHARACTERS),
     M8("M8", 115200, 0, null, false, null, false, -1, false, true, 4, 0, NO_SPECIAL_CHARACTERS),
     P5("P5", 115200, 20, 96, true, 10, 10, false, -1, true, false, true, 4, 0, NO_SPECIAL_CHARACTERS),
     S5("S5", 115200, 0, null, false, null, false, -1, false, true, 4, 0, NO_SPECIAL_CHARACTERS),
@@ -101,9 +104,11 @@ public enum RotelModel {
     private int sourceCategory;
     private int nbAdditionalZones;
     private boolean additionalCommands;
+    private boolean powerControlPerZone;
     private @Nullable Integer volumeMax;
     private boolean directVolume;
     private @Nullable Integer toneLevelMax;
+    private boolean getBypassStatusAvailable;
     private boolean playControl;
     private @Nullable RotelCommand zoneSelectCmd;
     private int dspCategory;
@@ -144,9 +149,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, 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);
+        this(name, baudRate, DISPLAY_REFRESH, sourceCategory, nbAdditionalZones, additionalCommands, true, volumeMax,
+                directVolume, toneLevelMax, false, null, playControl, zoneSelectCmd, dspCategory, false, false, false,
+                null, null, deviceId, respNbChars, respNbFlags, charsBeforeFlags, flagsMapping, NO_SPECIAL_CHARACTERS);
     }
 
     /**
@@ -170,9 +175,10 @@ 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, 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);
+        this(name, baudRate, POWER, sourceCategory, 0, false, false, volumeMax, directVolume, toneLevelMax,
+                toneLevelMax != null, null, playControl, null, dspCategory, getFrequencyAvailable, false,
+                getDimmerLevelAvailable, diummerLevelMin, diummerLevelMax, (byte) 0, 0, 0, false,
+                RotelFlagsMapping.NO_MAPPING, specialCharacters);
     }
 
     /**
@@ -198,10 +204,10 @@ 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, 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);
+        this(name, baudRate, POWER, sourceCategory, 0, false, false, volumeMax, directVolume, toneLevelMax,
+                toneLevelMax != null, balanceLevelMax, playControl, null, dspCategory, getFrequencyAvailable,
+                getSpeakerGroupsAvailable, getDimmerLevelAvailable, diummerLevelMin, diummerLevelMax, (byte) 0, 0, 0,
+                false, RotelFlagsMapping.NO_MAPPING, specialCharacters);
     }
 
     /**
@@ -213,9 +219,11 @@ public enum RotelModel {
      * @param sourceCategory the category from {@link RotelSource}
      * @param nbAdditionalZones the number of additional zones
      * @param additionalCommands true if other than primary commands are available
+     * @param powerControlPerZone true if device supports power control per zone
      * @param volumeMax the maximum volume or null if no volume management is available
      * @param directVolume true if a command to set the volume with a value is available
      * @param toneLevelMax the maximum tone level or null if no bass/treble management is available
+     * @param getBypassStatusAvailable true if the command to get the bypass status for tone control is available
      * @param balanceLevelMax the maximum balance level or null if no balance management is available
      * @param playControl true if control of source playback is available
      * @param zoneSelectCmd the command to be used to select a zone
@@ -233,9 +241,9 @@ public enum RotelModel {
      * @param specialCharacters the table of special characters that can be found in the standard response message
      */
     private RotelModel(String name, int baudRate, RotelCommand powerStateCmd, int sourceCategory, int nbAdditionalZones,
-            boolean additionalCommands, @Nullable Integer volumeMax, boolean directVolume,
-            @Nullable Integer toneLevelMax, @Nullable Integer balanceLevelMax, boolean playControl,
-            @Nullable RotelCommand zoneSelectCmd, int dspCategory, boolean getFrequencyAvailable,
+            boolean additionalCommands, boolean powerControlPerZone, @Nullable Integer volumeMax, boolean directVolume,
+            @Nullable Integer toneLevelMax, boolean getBypassStatusAvailable, @Nullable Integer balanceLevelMax,
+            boolean playControl, @Nullable RotelCommand zoneSelectCmd, int dspCategory, boolean getFrequencyAvailable,
             boolean getSpeakerGroupsAvailable, boolean getDimmerLevelAvailable, @Nullable Integer diummerLevelMin,
             @Nullable Integer diummerLevelMax, byte deviceId, int respNbChars, int respNbFlags,
             boolean charsBeforeFlags, RotelFlagsMapping flagsMapping, byte[][] specialCharacters) {
@@ -245,9 +253,11 @@ public enum RotelModel {
         this.sourceCategory = sourceCategory;
         this.nbAdditionalZones = nbAdditionalZones;
         this.additionalCommands = additionalCommands;
+        this.powerControlPerZone = powerControlPerZone;
         this.volumeMax = volumeMax;
         this.directVolume = directVolume;
         this.toneLevelMax = toneLevelMax;
+        this.getBypassStatusAvailable = getBypassStatusAvailable;
         this.balanceLevelMax = balanceLevelMax;
         this.playControl = playControl;
         this.zoneSelectCmd = zoneSelectCmd;
@@ -302,12 +312,16 @@ public enum RotelModel {
     }
 
     /**
-     * Get the number of additional zones
+     * Get the number of zones
      *
-     * @return the number of additional zones
+     * @return the number of zones
      */
-    public int getNbAdditionalZones() {
-        return nbAdditionalZones;
+    public int getNumberOfZones() {
+        return nbAdditionalZones + 1;
+    }
+
+    private boolean isZoneAvailable(int numZone) {
+        return numZone >= 1 && numZone <= getNumberOfZones();
     }
 
     /**
@@ -320,57 +334,40 @@ public enum RotelModel {
     }
 
     /**
-     * Inform whether zone 2 commands are available
+     * Inform whether zone N commands are available
      *
-     * @return true if zone 2 commands are available
-     */
-    public boolean hasZone2Commands() {
-        return nbAdditionalZones >= 1 && additionalCommands;
-    }
-
-    /**
-     * Inform whether zone 3 commands are available
+     * @param numZone the zone number, 1 for for zone 1 until 4 for zone 4
      *
-     * @return true if zone 3 commands are available
+     * @return true if zone N commands are available
      */
-    public boolean hasZone3Commands() {
-        return nbAdditionalZones >= 2 && additionalCommands;
+    public boolean hasZoneCommands(int numZone) {
+        if (numZone < 1 || numZone > MAX_NUMBER_OF_ZONES) {
+            throw new IllegalArgumentException("numZone must be in range 1-" + MAX_NUMBER_OF_ZONES);
+        }
+        return additionalCommands && isZoneAvailable(numZone);
     }
 
     /**
-     * Inform whether zone 4 commands are available
+     * Inform whether source control is available in a zone
      *
-     * @return true if zone 4 commands are available
-     */
-    public boolean hasZone4Commands() {
-        return nbAdditionalZones >= 3 && additionalCommands;
-    }
-
-    /**
-     * Inform whether source control is available in the zone 2
+     * @param numZone the zone number, 1 for zone 1 until 4 for zone 4
      *
      * @return true if source control is available
      */
-    public boolean hasZone2SourceControl() {
-        return sourceCategory >= 1 && nbAdditionalZones >= 1;
-    }
-
-    /**
-     * Inform whether source control is available in the zone 3
-     *
-     * @return true if source control is available
-     */
-    public boolean hasZone3SourceControl() {
-        return sourceCategory >= 1 && nbAdditionalZones >= 2;
+    public boolean hasZoneSourceControl(int numZone) {
+        if (numZone < 1 || numZone > MAX_NUMBER_OF_ZONES) {
+            throw new IllegalArgumentException("numZone must be in range 1-" + MAX_NUMBER_OF_ZONES);
+        }
+        return hasSourceControl() && isZoneAvailable(numZone);
     }
 
     /**
-     * Inform whether source control is available in the zone 4
+     * Inform whether device supports power control per zone
      *
-     * @return true if source control is available
+     * @return true if device supports power control per zone
      */
-    public boolean hasZone4SourceControl() {
-        return sourceCategory >= 1 && nbAdditionalZones >= 3;
+    public boolean hasPowerControlPerZone() {
+        return powerControlPerZone;
     }
 
     /**
@@ -410,6 +407,15 @@ public enum RotelModel {
         return toneLevelMax != null;
     }
 
+    /**
+     * Inform whether the command to get the current bypass status for tone control is available
+     *
+     * @return true if the command is available
+     */
+    public boolean canGetBypassStatus() {
+        return getBypassStatusAvailable;
+    }
+
     /**
      * Get the maximum tone level
      *
@@ -577,40 +583,17 @@ public enum RotelModel {
     }
 
     /**
-     * Get the list of available {@link RotelSource} in the main zone
+     * Get the list of available {@link RotelSource} in a zone
      *
-     * @return the list of available {@link RotelSource} in the main zone
-     */
-    public List<RotelSource> getMainZoneSources() {
-        return (hasSourceControl() && hasOtherThanPrimaryCommands()) ? RotelSource.getSources(sourceCategory, 1)
-                : new ArrayList<>();
-    }
-
-    /**
-     * Get the list of available {@link RotelSource} in the zone 2
+     * @param numZone the zone number, 1 for zone 1 until 4 for zone 4
      *
      * @return the list of available {@link RotelSource} in the zone 2
      */
-    public List<RotelSource> getZone2Sources() {
-        return hasZone2SourceControl() ? RotelSource.getSources(sourceCategory, 2) : new ArrayList<>();
-    }
-
-    /**
-     * Get the list of available {@link RotelSource} in the zone 3
-     *
-     * @return the list of available {@link RotelSource} in the zone 3
-     */
-    public List<RotelSource> getZone3Sources() {
-        return hasZone3SourceControl() ? RotelSource.getSources(sourceCategory, 3) : new ArrayList<>();
-    }
-
-    /**
-     * Get the list of available {@link RotelSource} in the zone 4
-     *
-     * @return the list of available {@link RotelSource} in the zone 4
-     */
-    public List<RotelSource> getZone4Sources() {
-        return hasZone4SourceControl() ? RotelSource.getSources(sourceCategory, 4) : new ArrayList<>();
+    public List<RotelSource> getZoneSources(int numZone) {
+        if (numZone < 1 || numZone > MAX_NUMBER_OF_ZONES) {
+            throw new IllegalArgumentException("numZone must be in range 1-" + MAX_NUMBER_OF_ZONES);
+        }
+        return hasZoneSourceControl(numZone) ? RotelSource.getSources(sourceCategory, numZone) : new ArrayList<>();
     }
 
     /**
@@ -649,55 +632,20 @@ public enum RotelModel {
     }
 
     /**
-     * Get the main zone source associated to a command
+     * Get the zone N source associated to a command
      *
-     * @param command the command used to identify the main zone source
+     * @param command the command used to identify the zone N source
+     * @param numZone the zone number, 1 for zone 1 until 4 for zone 4
      *
-     * @return the main zone source associated to the searched command
+     * @return the zone N source associated to the searched command
      *
-     * @throws RotelException - If no main zone source is associated to the searched command
+     * @throws RotelException - If no zone N source is associated to the searched command
      */
-    public RotelSource getMainZoneSourceFromCommand(RotelCommand command) throws RotelException {
-        return RotelSource.getFromCommand(sourceCategory, command, 1);
-    }
-
-    /**
-     * Get the zone 2 source associated to a command
-     *
-     * @param command the command used to identify the zone 2 source
-     *
-     * @return the zone 2 source associated to the searched command
-     *
-     * @throws RotelException - If no zone 2 source is associated to the searched command
-     */
-    public RotelSource getZone2SourceFromCommand(RotelCommand command) throws RotelException {
-        return RotelSource.getFromCommand(sourceCategory, command, 2);
-    }
-
-    /**
-     * Get the zone 3 source associated to a command
-     *
-     * @param command the command used to identify the zone 3 source
-     *
-     * @return the zone 3 source associated to the searched command
-     *
-     * @throws RotelException - If no zone 3 source is associated to the searched command
-     */
-    public RotelSource getZone3SourceFromCommand(RotelCommand command) throws RotelException {
-        return RotelSource.getFromCommand(sourceCategory, command, 3);
-    }
-
-    /**
-     * Get the zone 4 source associated to a command
-     *
-     * @param command the command used to identify the zone 4 source
-     *
-     * @return the zone 4 source associated to the searched command
-     *
-     * @throws RotelException - If no zone 4 source is associated to the searched command
-     */
-    public RotelSource getZone4SourceFromCommand(RotelCommand command) throws RotelException {
-        return RotelSource.getFromCommand(sourceCategory, command, 4);
+    public RotelSource getZoneSourceFromCommand(RotelCommand command, int numZone) throws RotelException {
+        if (numZone < 1 || numZone > MAX_NUMBER_OF_ZONES) {
+            throw new IllegalArgumentException("numZone must be in range 1-" + MAX_NUMBER_OF_ZONES);
+        }
+        return RotelSource.getFromCommand(sourceCategory, command, numZone);
     }
 
     /**
index 6d2a561c200364a51903c8d250304a78bc2e18dc..48cce171f789c5eb249ce51df0302df1ee9132f2 100644 (file)
@@ -58,24 +58,30 @@ public enum RotelCommand {
     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),
+    ZONE1_VOLUME_UP("Zone 1 Volume Up", null, "z1:vol_up"),
+    ZONE1_VOLUME_DOWN("Zone 1 Volume Down", null, "z1:vol_dwn"),
+    ZONE1_VOLUME_SET("Set Zone 1 Volume to level", null, "z1:vol_"),
+    ZONE1_MUTE_TOGGLE("Zone 1 Mute Toggle", null, "z1:mute"),
+    ZONE1_MUTE_ON("Zone 1 Mute On", null, "z1:mute_on"),
+    ZONE1_MUTE_OFF("Zone 1 Mute Off", null, "z1:mute_off"),
+    ZONE2_VOLUME_UP("Zone 2 Volume Up", ZONE2_CMD, (byte) 0, null, "z2:vol_up"),
+    ZONE2_VOLUME_DOWN("Zone 2 Volume Down", ZONE2_CMD, (byte) 1, null, "z2:vol_dwn"),
+    ZONE2_VOLUME_SET("Set Zone 2 Volume to level", ZONE2_VOLUME_CMD, (byte) 0, null, "z2:vol_"),
+    ZONE2_MUTE_TOGGLE("Zone 2 Mute Toggle", ZONE2_CMD, (byte) 0x1E, null, "z2:mute"),
+    ZONE2_MUTE_ON("Zone 2 Mute On", ZONE2_CMD, (byte) 0x6C, null, "z2:mute_on"),
+    ZONE2_MUTE_OFF("Zone 2 Mute Off", ZONE2_CMD, (byte) 0x6D, null, "z2:mute_off"),
+    ZONE3_VOLUME_UP("Zone 3 Volume Up", ZONE3_CMD, (byte) 0, null, "z3:vol_up"),
+    ZONE3_VOLUME_DOWN("Zone 3 Volume Down", ZONE3_CMD, (byte) 1, null, "z3:vol_dwn"),
+    ZONE3_VOLUME_SET("Set Zone 3 Volume to level", ZONE3_VOLUME_CMD, (byte) 0, null, "z3:vol_"),
+    ZONE3_MUTE_TOGGLE("Zone 3 Mute Toggle", ZONE3_CMD, (byte) 0x1E, null, "z3:mute"),
+    ZONE3_MUTE_ON("Zone 3 Mute On", ZONE3_CMD, (byte) 0x6C, null, "z3:mute_on"),
+    ZONE3_MUTE_OFF("Zone 3 Mute Off", ZONE3_CMD, (byte) 0x6D, null, "z3:mute_off"),
+    ZONE4_VOLUME_UP("Zone 4 Volume Up", ZONE4_CMD, (byte) 0, null, "z4:vol_up"),
+    ZONE4_VOLUME_DOWN("Zone 4 Volume Down", ZONE4_CMD, (byte) 1, null, "z4:vol_dwn"),
+    ZONE4_VOLUME_SET("Set Zone 4 Volume to level", ZONE4_VOLUME_CMD, (byte) 0, null, "z4:vol_"),
+    ZONE4_MUTE_TOGGLE("Zone 4 Mute Toggle", ZONE4_CMD, (byte) 0x1E, null, "z4:mute"),
+    ZONE4_MUTE_ON("Zone 4 Mute On", ZONE4_CMD, (byte) 0x6C, null, "z4:mute_on"),
+    ZONE4_MUTE_OFF("Zone 4 Mute Off", ZONE4_CMD, (byte) 0x6D, null, "z4:mute_off"),
     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"),
@@ -112,7 +118,12 @@ public enum RotelCommand {
     SOURCE_PLAYFI("Source PlayFi", "playfi", "playfi"),
     SOURCE_IRADIO("Source iRadio", "iradio", "iradio"),
     SOURCE_NETWORK("Source Network", "network", "network"),
+    SOURCE_INPUT_A("Source Input A", null, "input_a"),
+    SOURCE_INPUT_B("Source Input B", null, "input_b"),
+    SOURCE_INPUT_C("Source Input C", null, "input_c"),
+    SOURCE_INPUT_D("Source Input D", null, "input_d"),
     SOURCE("Request current source", "get_current_source", "source?"),
+    INPUT("Request current source", null, "input?"),
     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"),
@@ -131,6 +142,10 @@ public enum RotelCommand {
     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"),
+    ZONE1_SOURCE_INPUT_A("Zone 1 Source Input A", null, "z1:input_a"),
+    ZONE1_SOURCE_INPUT_B("Zone 1 Source Input B", null, "z1:input_b"),
+    ZONE1_SOURCE_INPUT_C("Zone 1 Source Input C", null, "z1:input_c"),
+    ZONE1_SOURCE_INPUT_D("Zone 1 Source Input D", null, "z1:input_d"),
     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"),
@@ -155,6 +170,10 @@ public enum RotelCommand {
     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"),
+    ZONE2_SOURCE_INPUT_A("Zone 2 Source Input A", null, "z2:input_a"),
+    ZONE2_SOURCE_INPUT_B("Zone 2 Source Input B", null, "z2:input_b"),
+    ZONE2_SOURCE_INPUT_C("Zone 2 Source Input C", null, "z2:input_c"),
+    ZONE2_SOURCE_INPUT_D("Zone 2 Source Input D", null, "z2:input_d"),
     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"),
@@ -167,6 +186,10 @@ public enum RotelCommand {
     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"),
+    ZONE3_SOURCE_INPUT_A("Zone 3 Source Input A", null, "z3:input_a"),
+    ZONE3_SOURCE_INPUT_B("Zone 3 Source Input B", null, "z3:input_b"),
+    ZONE3_SOURCE_INPUT_C("Zone 3 Source Input C", null, "z3:input_c"),
+    ZONE3_SOURCE_INPUT_D("Zone 3 Source Input D", null, "z3:input_d"),
     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"),
@@ -179,6 +202,10 @@ public enum RotelCommand {
     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"),
+    ZONE4_SOURCE_INPUT_A("Zone 4 Source Input A", null, "z4:input_a"),
+    ZONE4_SOURCE_INPUT_B("Zone 4 Source Input B", null, "z4:input_b"),
+    ZONE4_SOURCE_INPUT_C("Zone 4 Source Input C", null, "z4:input_c"),
+    ZONE4_SOURCE_INPUT_D("Zone 4 Source Input D", null, "z4:input_d"),
     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"),
@@ -210,6 +237,30 @@ public enum RotelCommand {
     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?"),
+    ZONE1_TREBLE_UP("Zone 1 Treble Up", null, "z1:treble_up"),
+    ZONE1_TREBLE_DOWN("Zone 1 Treble Down", null, "z1:treble_down"),
+    ZONE1_TREBLE_SET("Set Zone 1 Treble to level", null, "z1:treble_"),
+    ZONE1_BASS_UP("Zone 1 Bass Up", null, "z1:bass_up"),
+    ZONE1_BASS_DOWN("Zone 1 Bass Down", null, "z1:bass_down"),
+    ZONE1_BASS_SET("Set Zone 1 Bass to level", null, "z1:bass_"),
+    ZONE2_TREBLE_UP("Zone 2 Treble Up", null, "z2:treble_up"),
+    ZONE2_TREBLE_DOWN("Zone 2 Treble Down", null, "z2:treble_down"),
+    ZONE2_TREBLE_SET("Set Zone 2 Treble to level", null, "z2:treble_"),
+    ZONE2_BASS_UP("Zone 2 Bass Up", null, "z2:bass_up"),
+    ZONE2_BASS_DOWN("Zone 2 Bass Down", null, "z2:bass_down"),
+    ZONE2_BASS_SET("Set Zone 2 Bass to level", null, "z2:bass_"),
+    ZONE3_TREBLE_UP("Zone 3 Treble Up", null, "z3:treble_up"),
+    ZONE3_TREBLE_DOWN("Zone 3 Treble Down", null, "z3:treble_down"),
+    ZONE3_TREBLE_SET("Set Zone 3 Treble to level", null, "z3:treble_"),
+    ZONE3_BASS_UP("Zone 3 Bass Up", null, "z3:bass_up"),
+    ZONE3_BASS_DOWN("Zone 3 Bass Down", null, "z3:bass_down"),
+    ZONE3_BASS_SET("Set Zone 3 Bass to level", null, "z3:bass_"),
+    ZONE4_TREBLE_UP("Zone 4 Treble Up", null, "z4:treble_up"),
+    ZONE4_TREBLE_DOWN("Zone 4 Treble Down", null, "z4:treble_down"),
+    ZONE4_TREBLE_SET("Set Zone 4 Treble to level", null, "z4:treble_"),
+    ZONE4_BASS_UP("Zone 4 Bass Up", null, "z4:bass_up"),
+    ZONE4_BASS_DOWN("Zone 4 Bass Down", null, "z4:bass_down"),
+    ZONE4_BASS_SET("Set Zone 4 Bass to level", null, "z4:bass_"),
     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"),
@@ -234,6 +285,18 @@ public enum RotelCommand {
     BALANCE_RIGHT("Balance Right", "balance_right", "balance_r"),
     BALANCE_LEFT("Balance Left", "balance_left", "balance_l"),
     BALANCE_SET("Set Balance to level", "balance_", "balance_"),
+    ZONE1_BALANCE_RIGHT("Zone 1 Balance Right", null, "z1:balance_r"),
+    ZONE1_BALANCE_LEFT("Zone 1 Balance Left", null, "z1:balance_l"),
+    ZONE1_BALANCE_SET("Set Zone 1 Balance to level", null, "z1:balance_"),
+    ZONE2_BALANCE_RIGHT("Zone 2 Balance Right", null, "z2:balance_r"),
+    ZONE2_BALANCE_LEFT("Zone 2 Balance Left", null, "z2:balance_l"),
+    ZONE2_BALANCE_SET("Set Zone 2 Balance to level", null, "z2:balance_"),
+    ZONE3_BALANCE_RIGHT("Zone 3 Balance Right", null, "z3:balance_r"),
+    ZONE3_BALANCE_LEFT("Zone 3 Balance Left", null, "z3:balance_l"),
+    ZONE3_BALANCE_SET("Set Zone 3 Balance to level", null, "z3:balance_"),
+    ZONE4_BALANCE_RIGHT("Zone 4 Balance Right", null, "z4:balance_r"),
+    ZONE4_BALANCE_LEFT("Zone 4 Balance Left", null, "z4:balance_l"),
+    ZONE4_BALANCE_SET("Set Zone 4 Balance to level", null, "z4:balance_"),
     BALANCE("Request current balance setting", "get_balance", "balance?"),
     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"),
@@ -241,7 +304,9 @@ public enum RotelCommand {
     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?");
+    SPEAKER("Request current active speaker outputs", "get_current_speaker", "speaker?"),
+    MODEL("Request the model number", null, "model?"),
+    VERSION("Request the main CPU software version", null, "version?");
 
     public static final byte PRIMARY_COMMAND = (byte) 0x10;
 
index 2062aceed082e0b034c360cd0efb2a1c2de525fc..1821d0294cf8a98352fe4680f86b48af0e2566a1 100644 (file)
@@ -18,8 +18,10 @@ import static org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHa
 import java.io.InterruptedIOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.StringJoiner;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -53,28 +55,20 @@ public class RotelSimuConnector extends RotelConnector {
     private byte[] feedbackMsg = new byte[1];
     private int idxInFeedbackMsg = feedbackMsg.length;
 
-    private boolean power;
-    private boolean powerZone2;
-    private boolean powerZone3;
-    private boolean powerZone4;
-    private RotelSource source = RotelSource.CAT0_CD;
-    private RotelSource recordSource = RotelSource.CAT1_CD;
-    private RotelSource sourceZone2 = RotelSource.CAT1_CD;
-    private RotelSource sourceZone3 = RotelSource.CAT1_CD;
-    private RotelSource sourceZone4 = RotelSource.CAT1_CD;
+    private boolean[] powers = { false, false, false, false, false };
+    private RotelSource[] sources;
+    private RotelSource recordSource;
     private boolean multiinput;
     private RotelDsp dsp = RotelDsp.CAT4_NONE;
-    private int volume = 50;
-    private boolean mute;
-    private int volumeZone2 = 20;
-    private boolean muteZone2;
-    private int volumeZone3 = 30;
-    private boolean muteZone3;
-    private int volumeZone4 = 40;
-    private boolean muteZone4;
-    private int bass;
-    private int treble;
+    private int[] volumes = { 50, 10, 20, 30, 40 };
+    private boolean[] mutes = { false, false, false, false, false };
+    private boolean tcbypass;
+    private int[] basses = { 0, 0, 0, 0, 0 };
+    private int[] trebles = { 0, 0, 0, 0, 0 };
+    private int[] balances = { 0, 0, 0, 0, 0 };
     private boolean showTreble;
+    private boolean speakerA = true;
+    private boolean speakerB = false;
     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
     private int track = 1;
     private boolean selectingRecord;
@@ -85,6 +79,8 @@ public class RotelSimuConnector extends RotelConnector {
     private int maxVolume;
     private int minToneLevel;
     private int maxToneLevel;
+    private int minBalance;
+    private int maxBalance;
 
     /**
      * Constructor
@@ -104,6 +100,12 @@ public class RotelSimuConnector extends RotelConnector {
         this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
         this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
         this.minToneLevel = -this.maxToneLevel;
+        this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
+        this.minBalance = -this.maxBalance;
+        List<RotelSource> modelSources = model.getSources();
+        RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
+        sources = new RotelSource[] { source, source, source, source, source };
+        recordSource = source;
     }
 
     @Override
@@ -158,12 +160,96 @@ public class RotelSimuConnector extends RotelConnector {
         String textAscii = "";
         boolean accepted = true;
         boolean resetZone = true;
+        int numZone = 0;
+        switch (cmd) {
+            case ZONE1_VOLUME_UP:
+            case ZONE1_VOLUME_DOWN:
+            case ZONE1_VOLUME_SET:
+            case ZONE1_MUTE_TOGGLE:
+            case ZONE1_MUTE_ON:
+            case ZONE1_MUTE_OFF:
+            case ZONE1_BASS_UP:
+            case ZONE1_BASS_DOWN:
+            case ZONE1_BASS_SET:
+            case ZONE1_TREBLE_UP:
+            case ZONE1_TREBLE_DOWN:
+            case ZONE1_TREBLE_SET:
+            case ZONE1_BALANCE_LEFT:
+            case ZONE1_BALANCE_RIGHT:
+            case ZONE1_BALANCE_SET:
+                numZone = 1;
+                break;
+            case ZONE2_POWER_OFF:
+            case ZONE2_POWER_ON:
+            case ZONE2_VOLUME_UP:
+            case ZONE2_VOLUME_DOWN:
+            case ZONE2_VOLUME_SET:
+            case ZONE2_MUTE_TOGGLE:
+            case ZONE2_MUTE_ON:
+            case ZONE2_MUTE_OFF:
+            case ZONE2_BASS_UP:
+            case ZONE2_BASS_DOWN:
+            case ZONE2_BASS_SET:
+            case ZONE2_TREBLE_UP:
+            case ZONE2_TREBLE_DOWN:
+            case ZONE2_TREBLE_SET:
+            case ZONE2_BALANCE_LEFT:
+            case ZONE2_BALANCE_RIGHT:
+            case ZONE2_BALANCE_SET:
+                numZone = 2;
+                break;
+            case ZONE3_POWER_OFF:
+            case ZONE3_POWER_ON:
+            case ZONE3_VOLUME_UP:
+            case ZONE3_VOLUME_DOWN:
+            case ZONE3_VOLUME_SET:
+            case ZONE3_MUTE_TOGGLE:
+            case ZONE3_MUTE_ON:
+            case ZONE3_MUTE_OFF:
+            case ZONE3_BASS_UP:
+            case ZONE3_BASS_DOWN:
+            case ZONE3_BASS_SET:
+            case ZONE3_TREBLE_UP:
+            case ZONE3_TREBLE_DOWN:
+            case ZONE3_TREBLE_SET:
+            case ZONE3_BALANCE_LEFT:
+            case ZONE3_BALANCE_RIGHT:
+            case ZONE3_BALANCE_SET:
+                numZone = 3;
+                break;
+            case ZONE4_POWER_OFF:
+            case ZONE4_POWER_ON:
+            case ZONE4_VOLUME_UP:
+            case ZONE4_VOLUME_DOWN:
+            case ZONE4_VOLUME_SET:
+            case ZONE4_MUTE_TOGGLE:
+            case ZONE4_MUTE_ON:
+            case ZONE4_MUTE_OFF:
+            case ZONE4_BASS_UP:
+            case ZONE4_BASS_DOWN:
+            case ZONE4_BASS_SET:
+            case ZONE4_TREBLE_UP:
+            case ZONE4_TREBLE_DOWN:
+            case ZONE4_TREBLE_SET:
+            case ZONE4_BALANCE_LEFT:
+            case ZONE4_BALANCE_RIGHT:
+            case ZONE4_BALANCE_SET:
+                numZone = 4;
+                break;
+            default:
+                break;
+        }
         switch (cmd) {
             case DISPLAY_REFRESH:
                 break;
             case POWER_OFF:
             case MAIN_ZONE_POWER_OFF:
-                power = false;
+                powers[0] = false;
+                if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
+                    for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                        powers[zone] = false;
+                    }
+                }
                 text = buildSourceLine1Response();
                 textLine1Left = buildSourceLine1LeftResponse();
                 textLine1Right = buildVolumeLine1RightResponse();
@@ -171,7 +257,12 @@ public class RotelSimuConnector extends RotelConnector {
                 break;
             case POWER_ON:
             case MAIN_ZONE_POWER_ON:
-                power = true;
+                powers[0] = true;
+                if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
+                    for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                        powers[zone] = true;
+                    }
+                }
                 text = buildSourceLine1Response();
                 textLine1Left = buildSourceLine1LeftResponse();
                 textLine1Right = buildVolumeLine1RightResponse();
@@ -181,49 +272,27 @@ public class RotelSimuConnector extends RotelConnector {
                 textAscii = buildPowerAsciiResponse();
                 break;
             case ZONE2_POWER_OFF:
-                powerZone2 = false;
-                text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                        powerZone2, sourceZone2);
-                showZone = 2;
-                resetZone = false;
-                break;
-            case ZONE2_POWER_ON:
-                powerZone2 = true;
-                text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                        powerZone2, sourceZone2);
-                showZone = 2;
-                resetZone = false;
-                break;
             case ZONE3_POWER_OFF:
-                powerZone3 = false;
-                text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
-                showZone = 3;
-                resetZone = false;
-                break;
-            case ZONE3_POWER_ON:
-                powerZone3 = true;
-                text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
-                showZone = 3;
-                resetZone = false;
-                break;
             case ZONE4_POWER_OFF:
-                powerZone4 = false;
-                text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
-                showZone = 4;
+                powers[numZone] = false;
+                text = textLine2 = buildZonePowerResponse(numZone);
+                showZone = numZone;
                 resetZone = false;
                 break;
+            case ZONE2_POWER_ON:
+            case ZONE3_POWER_ON:
             case ZONE4_POWER_ON:
-                powerZone4 = true;
-                text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
-                showZone = 4;
+                powers[numZone] = true;
+                text = textLine2 = buildZonePowerResponse(numZone);
+                showZone = numZone;
                 resetZone = false;
                 break;
             case RECORD_FONCTION_SELECT:
-                if (model.getNbAdditionalZones() >= 1 && model.getZoneSelectCmd() == cmd) {
+                if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
                     showZone++;
-                    if (showZone > model.getNbAdditionalZones()) {
+                    if (showZone >= model.getNumberOfZones()) {
                         showZone = 1;
-                        if (!power) {
+                        if (!powers[0]) {
                             showZone++;
                         }
                     }
@@ -231,53 +300,34 @@ public class RotelSimuConnector extends RotelConnector {
                     showZone = 1;
                 }
                 if (showZone == 1) {
-                    selectingRecord = power;
+                    selectingRecord = powers[0];
                     showTreble = false;
                     textLine2 = buildRecordResponse();
-                } else if (showZone == 2) {
-                    selectingRecord = false;
-                    text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            powerZone2, sourceZone2);
-                } else if (showZone == 3) {
-                    selectingRecord = false;
-                    text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
-                } else if (showZone == 4) {
+                } else if (showZone >= 2 && showZone <= 4) {
                     selectingRecord = false;
-                    text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
+                    text = textLine2 = buildZonePowerResponse(showZone);
                 }
                 resetZone = false;
                 break;
             case ZONE_SELECT:
-                if (model.getNbAdditionalZones() == 0
-                        || (model.getNbAdditionalZones() > 1 && model.getZoneSelectCmd() == cmd)
+                if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
                         || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
                     accepted = false;
                 } else {
                     if (model.getZoneSelectCmd() == cmd) {
-                        if (!power && !powerZone2) {
+                        if (!powers[0] && !powers[2]) {
                             showZone = 2;
-                            powerZone2 = true;
+                            powers[2] = true;
                         } else if (showZone == 2) {
-                            powerZone2 = !powerZone2;
+                            powers[2] = !powers[2];
                         } else {
                             showZone = 2;
                         }
-                    } else {
-                        if (showZone == 2) {
-                            powerZone2 = !powerZone2;
-                        } else if (showZone == 3) {
-                            powerZone3 = !powerZone3;
-                        } else if (showZone == 4) {
-                            powerZone4 = !powerZone4;
-                        }
+                    } else if (showZone >= 2 && showZone <= 4) {
+                        powers[showZone] = !powers[showZone];
                     }
-                    if (showZone == 2) {
-                        text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                                powerZone2, sourceZone2);
-                    } else if (showZone == 3) {
-                        text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
-                    } else if (showZone == 4) {
-                        text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
+                    if (showZone >= 2 && showZone <= 4) {
+                        text = textLine2 = buildZonePowerResponse(showZone);
                     }
                     resetZone = false;
                 }
@@ -286,193 +336,190 @@ public class RotelSimuConnector extends RotelConnector {
                 accepted = false;
                 break;
         }
-        if (!accepted && powerZone2) {
+        if (!accepted && numZone > 0 && powers[numZone]) {
             accepted = true;
             switch (cmd) {
+                case ZONE1_VOLUME_UP:
                 case ZONE2_VOLUME_UP:
-                    if (volumeZone2 < maxVolume) {
-                        volumeZone2++;
+                case ZONE3_VOLUME_UP:
+                case ZONE4_VOLUME_UP:
+                    if (volumes[numZone] < maxVolume) {
+                        volumes[numZone]++;
                     }
-                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            muteZone2, volumeZone2);
+                    text = textLine2 = buildZoneVolumeResponse(numZone);
+                    textAscii = buildVolumeAsciiResponse();
                     break;
+                case ZONE1_VOLUME_DOWN:
                 case ZONE2_VOLUME_DOWN:
-                    if (volumeZone2 > minVolume) {
-                        volumeZone2--;
+                case ZONE3_VOLUME_DOWN:
+                case ZONE4_VOLUME_DOWN:
+                    if (volumes[numZone] > minVolume) {
+                        volumes[numZone]--;
                     }
-                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            muteZone2, volumeZone2);
+                    text = textLine2 = buildZoneVolumeResponse(numZone);
+                    textAscii = buildVolumeAsciiResponse();
                     break;
+                case ZONE1_VOLUME_SET:
                 case ZONE2_VOLUME_SET:
+                case ZONE3_VOLUME_SET:
+                case ZONE4_VOLUME_SET:
                     if (value != null) {
-                        volumeZone2 = value;
-                    }
-                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            muteZone2, volumeZone2);
-                    break;
-                case VOLUME_UP:
-                    if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
-                        if (volumeZone2 < maxVolume) {
-                            volumeZone2++;
-                        }
-                        text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                                muteZone2, volumeZone2);
-                        resetZone = false;
-                    } else {
-                        accepted = false;
-                    }
-                    break;
-                case VOLUME_DOWN:
-                    if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
-                        if (volumeZone2 > minVolume) {
-                            volumeZone2--;
-                        }
-                        text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                                muteZone2, volumeZone2);
-                        resetZone = false;
-                    } else {
-                        accepted = false;
-                    }
-                    break;
-                case VOLUME_SET:
-                    if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
-                        if (value != null) {
-                            volumeZone2 = value;
-                        }
-                        text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                                muteZone2, volumeZone2);
-                        resetZone = false;
-                    } else {
-                        accepted = false;
+                        volumes[numZone] = value;
                     }
+                    text = textLine2 = buildZoneVolumeResponse(numZone);
+                    textAscii = buildVolumeAsciiResponse();
                     break;
+                case ZONE1_MUTE_TOGGLE:
                 case ZONE2_MUTE_TOGGLE:
-                    muteZone2 = !muteZone2;
-                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            muteZone2, volumeZone2);
+                case ZONE3_MUTE_TOGGLE:
+                case ZONE4_MUTE_TOGGLE:
+                    mutes[numZone] = !mutes[numZone];
+                    text = textLine2 = buildZoneVolumeResponse(numZone);
+                    textAscii = buildMuteAsciiResponse();
                     break;
+                case ZONE1_MUTE_ON:
                 case ZONE2_MUTE_ON:
-                    muteZone2 = true;
-                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            muteZone2, volumeZone2);
+                case ZONE3_MUTE_ON:
+                case ZONE4_MUTE_ON:
+                    mutes[numZone] = true;
+                    text = textLine2 = buildZoneVolumeResponse(numZone);
+                    textAscii = buildMuteAsciiResponse();
                     break;
+                case ZONE1_MUTE_OFF:
                 case ZONE2_MUTE_OFF:
-                    muteZone2 = false;
-                    text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            muteZone2, volumeZone2);
+                case ZONE3_MUTE_OFF:
+                case ZONE4_MUTE_OFF:
+                    mutes[numZone] = false;
+                    text = textLine2 = buildZoneVolumeResponse(numZone);
+                    textAscii = buildMuteAsciiResponse();
                     break;
-                default:
-                    accepted = false;
+                case ZONE1_BASS_UP:
+                case ZONE2_BASS_UP:
+                case ZONE3_BASS_UP:
+                case ZONE4_BASS_UP:
+                    if (!tcbypass && basses[numZone] < maxToneLevel) {
+                        basses[numZone] += STEP_TONE_LEVEL;
+                    }
+                    textAscii = buildBassAsciiResponse();
                     break;
-            }
-            if (!accepted) {
-                try {
-                    sourceZone2 = model.getZone2SourceFromCommand(cmd);
-                    powerZone2 = true;
-                    text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            powerZone2, sourceZone2);
-                    muteZone2 = false;
-                    accepted = true;
-                    showZone = 2;
-                    resetZone = false;
-                } catch (RotelException e) {
-                }
-            }
-            if (!accepted && !model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
-                try {
-                    sourceZone2 = model.getSourceFromCommand(cmd);
-                    powerZone2 = true;
-                    text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
-                            powerZone2, sourceZone2);
-                    muteZone2 = false;
-                    accepted = true;
-                    resetZone = false;
-                } catch (RotelException e) {
-                }
-            }
-        }
-        if (!accepted && powerZone3) {
-            accepted = true;
-            switch (cmd) {
-                case ZONE3_VOLUME_UP:
-                    if (volumeZone3 < maxVolume) {
-                        volumeZone3++;
+                case ZONE1_BASS_DOWN:
+                case ZONE2_BASS_DOWN:
+                case ZONE3_BASS_DOWN:
+                case ZONE4_BASS_DOWN:
+                    if (!tcbypass && basses[numZone] > minToneLevel) {
+                        basses[numZone] -= STEP_TONE_LEVEL;
                     }
-                    text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
+                    textAscii = buildBassAsciiResponse();
                     break;
-                case ZONE3_VOLUME_DOWN:
-                    if (volumeZone3 > minVolume) {
-                        volumeZone3--;
+                case ZONE1_BASS_SET:
+                case ZONE2_BASS_SET:
+                case ZONE3_BASS_SET:
+                case ZONE4_BASS_SET:
+                    if (!tcbypass && value != null) {
+                        basses[numZone] = value;
                     }
-                    text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
+                    textAscii = buildBassAsciiResponse();
                     break;
-                case ZONE3_VOLUME_SET:
-                    if (value != null) {
-                        volumeZone3 = value;
+                case ZONE1_TREBLE_UP:
+                case ZONE2_TREBLE_UP:
+                case ZONE3_TREBLE_UP:
+                case ZONE4_TREBLE_UP:
+                    if (!tcbypass && trebles[numZone] < maxToneLevel) {
+                        trebles[numZone] += STEP_TONE_LEVEL;
                     }
-                    text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
+                    textAscii = buildTrebleAsciiResponse();
                     break;
-                case ZONE3_MUTE_TOGGLE:
-                    muteZone3 = !muteZone3;
-                    text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
+                case ZONE1_TREBLE_DOWN:
+                case ZONE2_TREBLE_DOWN:
+                case ZONE3_TREBLE_DOWN:
+                case ZONE4_TREBLE_DOWN:
+                    if (!tcbypass && trebles[numZone] > minToneLevel) {
+                        trebles[numZone] -= STEP_TONE_LEVEL;
+                    }
+                    textAscii = buildTrebleAsciiResponse();
                     break;
-                case ZONE3_MUTE_ON:
-                    muteZone3 = true;
-                    text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
+                case ZONE1_TREBLE_SET:
+                case ZONE2_TREBLE_SET:
+                case ZONE3_TREBLE_SET:
+                case ZONE4_TREBLE_SET:
+                    if (!tcbypass && value != null) {
+                        trebles[numZone] = value;
+                    }
+                    textAscii = buildTrebleAsciiResponse();
                     break;
-                case ZONE3_MUTE_OFF:
-                    muteZone3 = false;
-                    text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
+                case ZONE1_BALANCE_LEFT:
+                case ZONE2_BALANCE_LEFT:
+                case ZONE3_BALANCE_LEFT:
+                case ZONE4_BALANCE_LEFT:
+                    if (balances[numZone] > minBalance) {
+                        balances[numZone]--;
+                    }
+                    textAscii = buildBalanceAsciiResponse();
+                    break;
+                case ZONE1_BALANCE_RIGHT:
+                case ZONE2_BALANCE_RIGHT:
+                case ZONE3_BALANCE_RIGHT:
+                case ZONE4_BALANCE_RIGHT:
+                    if (balances[numZone] < maxBalance) {
+                        balances[numZone]++;
+                    }
+                    textAscii = buildBalanceAsciiResponse();
+                    break;
+                case ZONE1_BALANCE_SET:
+                case ZONE2_BALANCE_SET:
+                case ZONE3_BALANCE_SET:
+                case ZONE4_BALANCE_SET:
+                    if (value != null) {
+                        balances[numZone] = value;
+                    }
+                    textAscii = buildBalanceAsciiResponse();
                     break;
                 default:
                     accepted = false;
                     break;
             }
-            if (!accepted) {
-                try {
-                    sourceZone3 = model.getZone3SourceFromCommand(cmd);
-                    powerZone3 = true;
-                    text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
-                    muteZone3 = false;
-                    accepted = true;
-                    showZone = 3;
-                    resetZone = false;
-                } catch (RotelException e) {
+        }
+        if (!accepted) {
+            // Check if command is a change of source input for a zone
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                if (powers[zone]) {
+                    try {
+                        sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
+                        text = textLine2 = buildZonePowerResponse(zone);
+                        textAscii = buildSourceAsciiResponse();
+                        mutes[zone] = false;
+                        accepted = true;
+                        showZone = zone;
+                        resetZone = false;
+                        break;
+                    } catch (RotelException e) {
+                    }
                 }
             }
         }
-        if (!accepted && powerZone4) {
+        if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
             accepted = true;
             switch (cmd) {
-                case ZONE4_VOLUME_UP:
-                    if (volumeZone4 < maxVolume) {
-                        volumeZone4++;
+                case VOLUME_UP:
+                    if (volumes[2] < maxVolume) {
+                        volumes[2]++;
                     }
-                    text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
+                    text = textLine2 = buildZoneVolumeResponse(2);
+                    resetZone = false;
                     break;
-                case ZONE4_VOLUME_DOWN:
-                    if (volumeZone4 > minVolume) {
-                        volumeZone4--;
+                case VOLUME_DOWN:
+                    if (volumes[2] > minVolume) {
+                        volumes[2]--;
                     }
-                    text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
+                    text = textLine2 = buildZoneVolumeResponse(2);
+                    resetZone = false;
                     break;
-                case ZONE4_VOLUME_SET:
+                case VOLUME_SET:
                     if (value != null) {
-                        volumeZone4 = value;
+                        volumes[2] = value;
                     }
-                    text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
-                    break;
-                case ZONE4_MUTE_TOGGLE:
-                    muteZone4 = !muteZone4;
-                    text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
-                    break;
-                case ZONE4_MUTE_ON:
-                    muteZone4 = true;
-                    text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
-                    break;
-                case ZONE4_MUTE_OFF:
-                    muteZone4 = false;
-                    text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
+                    text = textLine2 = buildZoneVolumeResponse(2);
+                    resetZone = false;
                     break;
                 default:
                     accepted = false;
@@ -480,18 +527,16 @@ public class RotelSimuConnector extends RotelConnector {
             }
             if (!accepted) {
                 try {
-                    sourceZone4 = model.getZone4SourceFromCommand(cmd);
-                    powerZone4 = true;
-                    text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
-                    muteZone4 = false;
+                    sources[2] = model.getSourceFromCommand(cmd);
+                    text = textLine2 = buildZonePowerResponse(2);
+                    mutes[2] = false;
                     accepted = true;
-                    showZone = 4;
                     resetZone = false;
                 } catch (RotelException e) {
                 }
             }
         }
-        if (!accepted && power) {
+        if (!accepted && powers[0]) {
             accepted = true;
             switch (cmd) {
                 case UPDATE_AUTO:
@@ -510,8 +555,8 @@ public class RotelSimuConnector extends RotelConnector {
                     break;
                 case VOLUME_UP:
                 case MAIN_ZONE_VOLUME_UP:
-                    if (volume < maxVolume) {
-                        volume++;
+                    if (volumes[0] < maxVolume) {
+                        volumes[0]++;
                     }
                     text = buildVolumeLine1Response();
                     textLine1Right = buildVolumeLine1RightResponse();
@@ -519,8 +564,8 @@ public class RotelSimuConnector extends RotelConnector {
                     break;
                 case VOLUME_DOWN:
                 case MAIN_ZONE_VOLUME_DOWN:
-                    if (volume > minVolume) {
-                        volume--;
+                    if (volumes[0] > minVolume) {
+                        volumes[0]--;
                     }
                     text = buildVolumeLine1Response();
                     textLine1Right = buildVolumeLine1RightResponse();
@@ -528,7 +573,7 @@ public class RotelSimuConnector extends RotelConnector {
                     break;
                 case VOLUME_SET:
                     if (value != null) {
-                        volume = value;
+                        volumes[0] = value;
                     }
                     text = buildVolumeLine1Response();
                     textLine1Right = buildVolumeLine1RightResponse();
@@ -539,21 +584,21 @@ public class RotelSimuConnector extends RotelConnector {
                     break;
                 case MUTE_TOGGLE:
                 case MAIN_ZONE_MUTE_TOGGLE:
-                    mute = !mute;
+                    mutes[0] = !mutes[0];
                     text = buildSourceLine1Response();
                     textLine1Right = buildVolumeLine1RightResponse();
                     textAscii = buildMuteAsciiResponse();
                     break;
                 case MUTE_ON:
                 case MAIN_ZONE_MUTE_ON:
-                    mute = true;
+                    mutes[0] = true;
                     text = buildSourceLine1Response();
                     textLine1Right = buildVolumeLine1RightResponse();
                     textAscii = buildMuteAsciiResponse();
                     break;
                 case MUTE_OFF:
                 case MAIN_ZONE_MUTE_OFF:
-                    mute = false;
+                    mutes[0] = false;
                     text = buildSourceLine1Response();
                     textLine1Right = buildVolumeLine1RightResponse();
                     textAscii = buildMuteAsciiResponse();
@@ -562,27 +607,49 @@ public class RotelSimuConnector extends RotelConnector {
                     textAscii = buildMuteAsciiResponse();
                     break;
                 case TONE_MAX:
-                    textAscii = buildAsciiResponse(KEY_TONE_MAX, "%02d", maxToneLevel);
+                    textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
+                    break;
+                case TONE_CONTROLS_ON:
+                    tcbypass = false;
+                    textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
+                    break;
+                case TONE_CONTROLS_OFF:
+                    tcbypass = true;
+                    textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
+                    break;
+                case TONE_CONTROLS:
+                    textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
+                    break;
+                case TCBYPASS_ON:
+                    tcbypass = true;
+                    textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
+                    break;
+                case TCBYPASS_OFF:
+                    tcbypass = false;
+                    textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
+                    break;
+                case TCBYPASS:
+                    textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
                     break;
                 case BASS_UP:
-                    if (bass < maxToneLevel) {
-                        bass += STEP_TONE_LEVEL;
+                    if (!tcbypass && basses[0] < maxToneLevel) {
+                        basses[0] += STEP_TONE_LEVEL;
                     }
                     text = buildBassLine1Response();
                     textLine1Right = buildBassLine1RightResponse();
                     textAscii = buildBassAsciiResponse();
                     break;
                 case BASS_DOWN:
-                    if (bass > minToneLevel) {
-                        bass -= STEP_TONE_LEVEL;
+                    if (!tcbypass && basses[0] > minToneLevel) {
+                        basses[0] -= STEP_TONE_LEVEL;
                     }
                     text = buildBassLine1Response();
                     textLine1Right = buildBassLine1RightResponse();
                     textAscii = buildBassAsciiResponse();
                     break;
                 case BASS_SET:
-                    if (value != null) {
-                        bass = value;
+                    if (!tcbypass && value != null) {
+                        basses[0] = value;
                     }
                     text = buildBassLine1Response();
                     textLine1Right = buildBassLine1RightResponse();
@@ -592,24 +659,24 @@ public class RotelSimuConnector extends RotelConnector {
                     textAscii = buildBassAsciiResponse();
                     break;
                 case TREBLE_UP:
-                    if (treble < maxToneLevel) {
-                        treble += STEP_TONE_LEVEL;
+                    if (!tcbypass && trebles[0] < maxToneLevel) {
+                        trebles[0] += STEP_TONE_LEVEL;
                     }
                     text = buildTrebleLine1Response();
                     textLine1Right = buildTrebleLine1RightResponse();
                     textAscii = buildTrebleAsciiResponse();
                     break;
                 case TREBLE_DOWN:
-                    if (treble > minToneLevel) {
-                        treble -= STEP_TONE_LEVEL;
+                    if (!tcbypass && trebles[0] > minToneLevel) {
+                        trebles[0] -= STEP_TONE_LEVEL;
                     }
                     text = buildTrebleLine1Response();
                     textLine1Right = buildTrebleLine1RightResponse();
                     textAscii = buildTrebleAsciiResponse();
                     break;
                 case TREBLE_SET:
-                    if (value != null) {
-                        treble = value;
+                    if (!tcbypass && value != null) {
+                        trebles[0] = value;
                     }
                     text = buildTrebleLine1Response();
                     textLine1Right = buildTrebleLine1RightResponse();
@@ -628,6 +695,54 @@ public class RotelSimuConnector extends RotelConnector {
                         textLine1Right = buildBassLine1RightResponse();
                     }
                     break;
+                case BALANCE_LEFT:
+                    if (balances[0] > minBalance) {
+                        balances[0]--;
+                    }
+                    textAscii = buildBalanceAsciiResponse();
+                    break;
+                case BALANCE_RIGHT:
+                    if (balances[0] < maxBalance) {
+                        balances[0]++;
+                    }
+                    textAscii = buildBalanceAsciiResponse();
+                    break;
+                case BALANCE_SET:
+                    if (value != null) {
+                        balances[0] = value;
+                    }
+                    textAscii = buildBalanceAsciiResponse();
+                    break;
+                case BALANCE:
+                    textAscii = buildBalanceAsciiResponse();
+                    break;
+                case SPEAKER_A_TOGGLE:
+                    speakerA = !speakerA;
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
+                case SPEAKER_A_ON:
+                    speakerA = true;
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
+                case SPEAKER_A_OFF:
+                    speakerA = false;
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
+                case SPEAKER_B_TOGGLE:
+                    speakerB = !speakerB;
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
+                case SPEAKER_B_ON:
+                    speakerB = true;
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
+                case SPEAKER_B_OFF:
+                    speakerB = false;
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
+                case SPEAKER:
+                    textAscii = buildSpeakerAsciiResponse();
+                    break;
                 case PLAY:
                     playStatus = RotelPlayStatus.PLAYING;
                     textAscii = buildPlayStatusAsciiResponse();
@@ -669,14 +784,15 @@ public class RotelSimuConnector extends RotelConnector {
                     multiinput = !multiinput;
                     text = "MULTI IN " + (multiinput ? "ON" : "OFF");
                     try {
-                        source = model.getSourceFromCommand(cmd);
+                        sources[0] = model.getSourceFromCommand(cmd);
                         textLine1Left = buildSourceLine1LeftResponse();
                         textAscii = buildSourceAsciiResponse();
-                        mute = false;
+                        mutes[0] = false;
                     } catch (RotelException e) {
                     }
                     break;
                 case SOURCE:
+                case INPUT:
                     textAscii = buildSourceAsciiResponse();
                     break;
                 case STEREO:
@@ -779,7 +895,8 @@ public class RotelSimuConnector extends RotelConnector {
                     textAscii = buildDspAsciiResponse();
                     break;
                 case FREQUENCY:
-                    textAscii = buildAsciiResponse(KEY_FREQ, "44.1");
+                    textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
+                            : buildAsciiResponse(KEY_FREQ, "44.1");
                     break;
                 case DIMMER_LEVEL_SET:
                     if (value != null) {
@@ -790,13 +907,20 @@ public class RotelSimuConnector extends RotelConnector {
                 case DIMMER_LEVEL_GET:
                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
                     break;
+                case MODEL:
+                    textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
+                    break;
+                case VERSION:
+                    textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
+                    break;
                 default:
                     accepted = false;
                     break;
             }
             if (!accepted) {
+                // Check if command is a change of source input for the main zone
                 try {
-                    source = model.getMainZoneSourceFromCommand(cmd);
+                    sources[0] = model.getZoneSourceFromCommand(cmd, 1);
                     text = buildSourceLine1Response();
                     textLine1Left = buildSourceLine1LeftResponse();
                     textAscii = buildSourceAsciiResponse();
@@ -805,21 +929,23 @@ public class RotelSimuConnector extends RotelConnector {
                 }
             }
             if (!accepted) {
+                // Check if command is a change of source input
                 try {
                     if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
                         recordSource = model.getSourceFromCommand(cmd);
                     } else {
-                        source = model.getSourceFromCommand(cmd);
+                        sources[0] = model.getSourceFromCommand(cmd);
                     }
                     text = buildSourceLine1Response();
                     textLine1Left = buildSourceLine1LeftResponse();
                     textAscii = buildSourceAsciiResponse();
-                    mute = false;
+                    mutes[0] = false;
                     accepted = true;
                 } catch (RotelException e) {
                 }
             }
             if (!accepted) {
+                // Check if command is a change of record source
                 try {
                     recordSource = model.getRecordSourceFromCommand(cmd);
                     text = buildSourceLine1Response();
@@ -862,15 +988,15 @@ public class RotelSimuConnector extends RotelConnector {
             } catch (RotelException e) {
             }
             try {
-                model.setZone2(flags, powerZone2);
+                model.setZone2(flags, powers[2]);
             } catch (RotelException e) {
             }
             try {
-                model.setZone3(flags, powerZone3);
+                model.setZone3(flags, powers[3]);
             } catch (RotelException e) {
             }
             try {
-                model.setZone4(flags, powerZone4);
+                model.setZone4(flags, powers[4]);
             } catch (RotelException e) {
             }
             int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
@@ -919,51 +1045,113 @@ public class RotelSimuConnector extends RotelConnector {
     }
 
     private String buildAsciiResponse(String key, int value) {
-        return buildAsciiResponse(key, "%d", value);
+        return String.format("%s=%d", key, value);
     }
 
-    private String buildAsciiResponse(String key, String format, int value) {
-        return String.format("%s=" + format, key, value);
+    private String buildAsciiResponse(String key, boolean value) {
+        return buildAsciiResponse(key, buildOnOffValue(value));
     }
 
-    private String buildAsciiResponse(String key, boolean value) {
-        return buildAsciiResponse(key, value ? MSG_VALUE_ON : MSG_VALUE_OFF);
+    private String buildOnOffValue(boolean on) {
+        return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
     }
 
     private String buildPowerAsciiResponse() {
-        return buildAsciiResponse(KEY_POWER, power ? POWER_ON : STANDBY);
+        return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
     }
 
     private String buildVolumeAsciiResponse() {
-        return buildAsciiResponse(KEY_VOLUME, "%02d", volume);
+        if (model.getNumberOfZones() > 1) {
+            StringJoiner sj = new StringJoiner(",");
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                sj.add(String.format("%02d", volumes[zone]));
+            }
+            return buildAsciiResponse(KEY_VOLUME, sj.toString());
+        } else {
+            return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
+        }
     }
 
     private String buildMuteAsciiResponse() {
-        return buildAsciiResponse(KEY_MUTE, mute);
+        if (model.getNumberOfZones() > 1) {
+            StringJoiner sj = new StringJoiner(",");
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                sj.add(buildOnOffValue(mutes[zone]));
+            }
+            return buildAsciiResponse(KEY_MUTE, sj.toString());
+        } else {
+            return buildAsciiResponse(KEY_MUTE, mutes[0]);
+        }
     }
 
     private String buildBassAsciiResponse() {
-        String result;
-        if (bass == 0) {
-            result = buildAsciiResponse(KEY_BASS, "000");
-        } else if (bass > 0) {
-            result = buildAsciiResponse(KEY_BASS, "+%02d", bass);
+        if (model.getNumberOfZones() > 1) {
+            StringJoiner sj = new StringJoiner(",");
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                sj.add(buildBassTrebleValue(basses[zone]));
+            }
+            return buildAsciiResponse(KEY_BASS, sj.toString());
         } else {
-            result = buildAsciiResponse(KEY_BASS, "-%02d", -bass);
+            return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
         }
-        return result;
     }
 
     private String buildTrebleAsciiResponse() {
-        String result;
-        if (treble == 0) {
-            result = buildAsciiResponse(KEY_TREBLE, "000");
-        } else if (treble > 0) {
-            result = buildAsciiResponse(KEY_TREBLE, "+%02d", treble);
+        if (model.getNumberOfZones() > 1) {
+            StringJoiner sj = new StringJoiner(",");
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                sj.add(buildBassTrebleValue(trebles[zone]));
+            }
+            return buildAsciiResponse(KEY_TREBLE, sj.toString());
+        } else {
+            return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
+        }
+    }
+
+    private String buildBassTrebleValue(int value) {
+        if (tcbypass || value == 0) {
+            return "000";
+        } else if (value > 0) {
+            return String.format("+%02d", value);
         } else {
-            result = buildAsciiResponse(KEY_TREBLE, "-%02d", -treble);
+            return String.format("-%02d", -value);
         }
-        return result;
+    }
+
+    private String buildBalanceAsciiResponse() {
+        if (model.getNumberOfZones() > 1) {
+            StringJoiner sj = new StringJoiner(",");
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                sj.add(buildBalanceValue(balances[zone]));
+            }
+            return buildAsciiResponse(KEY_BALANCE, sj.toString());
+        } else {
+            return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
+        }
+    }
+
+    private String buildBalanceValue(int value) {
+        if (value == 0) {
+            return "000";
+        } else if (value > 0) {
+            return String.format("r%02d", value);
+        } else {
+            return String.format("l%02d", -value);
+        }
+    }
+
+    private String buildSpeakerAsciiResponse() {
+        String value;
+        if (speakerA && speakerB) {
+            value = MSG_VALUE_SPEAKER_AB;
+        } else if (speakerA && !speakerB) {
+            value = MSG_VALUE_SPEAKER_A;
+        } else if (!speakerA && speakerB) {
+            value = MSG_VALUE_SPEAKER_B;
+        } else {
+            value = MSG_VALUE_OFF;
+        }
+        return buildAsciiResponse(KEY_SPEAKER, value);
     }
 
     private String buildPlayStatusAsciiResponse() {
@@ -983,16 +1171,34 @@ public class RotelSimuConnector extends RotelConnector {
     }
 
     private String buildTrackAsciiResponse() {
-        return buildAsciiResponse(KEY_TRACK, "%03d", track);
+        return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
     }
 
     private String buildSourceAsciiResponse() {
+        if (model.getNumberOfZones() > 1) {
+            StringJoiner sj = new StringJoiner(",");
+            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                sj.add(buildZoneSourceValue(sources[zone]));
+            }
+            return buildAsciiResponse(KEY_INPUT, sj.toString());
+        } else {
+            return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
+        }
+    }
+
+    private String buildSourceValue(RotelSource source) {
         String str = null;
         RotelCommand command = source.getCommand();
         if (command != null) {
-            str = command.getAsciiCommandV2();
+            str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
         }
-        return buildAsciiResponse(KEY_SOURCE, (str == null) ? "" : str);
+        return str == null ? "" : str;
+    }
+
+    private String buildZoneSourceValue(RotelSource source) {
+        String str = buildSourceValue(source);
+        int idx = str.indexOf("input_");
+        return idx < 0 ? str : str.substring(idx + 6);
     }
 
     private String buildDspAsciiResponse() {
@@ -1001,29 +1207,29 @@ public class RotelSimuConnector extends RotelConnector {
 
     private String buildSourceLine1Response() {
         String text;
-        if (!power) {
+        if (!powers[0]) {
             text = "";
-        } else if (mute) {
+        } else if (mutes[0]) {
             text = "MUTE ON";
         } else {
-            text = getSourceLabel(source, false) + " " + getSourceLabel(recordSource, true);
+            text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
         }
         return text;
     }
 
     private String buildSourceLine1LeftResponse() {
         String text;
-        if (!power) {
+        if (!powers[0]) {
             text = "";
         } else {
-            text = getSourceLabel(source, false);
+            text = getSourceLabel(sources[0], false);
         }
         return text;
     }
 
     private String buildRecordResponse() {
         String text;
-        if (!power) {
+        if (!powers[0]) {
             text = "";
         } else {
             text = "REC " + getSourceLabel(recordSource, true);
@@ -1031,113 +1237,125 @@ public class RotelSimuConnector extends RotelConnector {
         return text;
     }
 
-    private String buildZonePowerResponse(String zone, boolean powerZone, RotelSource sourceZone) {
-        String state = powerZone ? getSourceLabel(sourceZone, true) : "OFF";
+    private String buildZonePowerResponse(int numZone) {
+        String zone;
+        if (numZone == 2) {
+            zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
+        } else {
+            zone = String.format("ZONE%d", numZone);
+        }
+        String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
         return zone + " " + state;
     }
 
     private String buildVolumeLine1Response() {
         String text;
-        if (volume == minVolume) {
+        if (volumes[0] == minVolume) {
             text = " VOLUME  MIN ";
-        } else if (volume == maxVolume) {
+        } else if (volumes[0] == maxVolume) {
             text = " VOLUME  MAX ";
         } else {
-            text = String.format(" VOLUME   %02d ", volume);
+            text = String.format(" VOLUME   %02d ", volumes[0]);
         }
         return text;
     }
 
     private String buildVolumeLine1RightResponse() {
         String text;
-        if (!power) {
+        if (!powers[0]) {
             text = "";
-        } else if (mute) {
+        } else if (mutes[0]) {
             text = "MUTE ON";
-        } else if (volume == minVolume) {
+        } else if (volumes[0] == minVolume) {
             text = "VOL MIN";
-        } else if (volume == maxVolume) {
+        } else if (volumes[0] == maxVolume) {
             text = "VOL MAX";
         } else {
-            text = String.format("VOL  %02d", volume);
+            text = String.format("VOL  %02d", volumes[0]);
         }
         return text;
     }
 
-    private String buildZoneVolumeResponse(String zone, boolean muted, int vol) {
+    private String buildZoneVolumeResponse(int numZone) {
+        String zone;
+        if (numZone == 2) {
+            zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
+        } else {
+            zone = String.format("ZONE%d", numZone);
+        }
         String text;
-        if (muted) {
+        if (mutes[numZone]) {
             text = zone + " MUTE ON";
-        } else if (vol == minVolume) {
+        } else if (volumes[numZone] == minVolume) {
             text = zone + " VOL MIN";
-        } else if (vol == maxVolume) {
+        } else if (volumes[numZone] == maxVolume) {
             text = zone + " VOL MAX";
         } else {
-            text = String.format("%s VOL %02d", zone, vol);
+            text = String.format("%s VOL %02d", zone, volumes[numZone]);
         }
         return text;
     }
 
     private String buildBassLine1Response() {
         String text;
-        if (bass == minToneLevel) {
+        if (basses[0] == minToneLevel) {
             text = "   BASS  MIN ";
-        } else if (bass == maxToneLevel) {
+        } else if (basses[0] == maxToneLevel) {
             text = "   BASS  MAX ";
-        } else if (bass == 0) {
+        } else if (basses[0] == 0) {
             text = "   BASS    0 ";
-        } else if (bass > 0) {
-            text = String.format("   BASS  +%02d ", bass);
+        } else if (basses[0] > 0) {
+            text = String.format("   BASS  +%02d ", basses[0]);
         } else {
-            text = String.format("   BASS  -%02d ", -bass);
+            text = String.format("   BASS  -%02d ", -basses[0]);
         }
         return text;
     }
 
     private String buildBassLine1RightResponse() {
         String text;
-        if (bass == minToneLevel) {
+        if (basses[0] == minToneLevel) {
             text = "LF  MIN";
-        } else if (bass == maxToneLevel) {
+        } else if (basses[0] == maxToneLevel) {
             text = "LF  MAX";
-        } else if (bass == 0) {
+        } else if (basses[0] == 0) {
             text = "LF    0";
-        } else if (bass > 0) {
-            text = String.format("LF + %02d", bass);
+        } else if (basses[0] > 0) {
+            text = String.format("LF + %02d", basses[0]);
         } else {
-            text = String.format("LF - %02d", -bass);
+            text = String.format("LF - %02d", -basses[0]);
         }
         return text;
     }
 
     private String buildTrebleLine1Response() {
         String text;
-        if (treble == minToneLevel) {
+        if (trebles[0] == minToneLevel) {
             text = " TREBLE  MIN ";
-        } else if (treble == maxToneLevel) {
+        } else if (trebles[0] == maxToneLevel) {
             text = " TREBLE  MAX ";
-        } else if (treble == 0) {
+        } else if (trebles[0] == 0) {
             text = " TREBLE    0 ";
-        } else if (treble > 0) {
-            text = String.format(" TREBLE  +%02d ", treble);
+        } else if (trebles[0] > 0) {
+            text = String.format(" TREBLE  +%02d ", trebles[0]);
         } else {
-            text = String.format(" TREBLE  -%02d ", -treble);
+            text = String.format(" TREBLE  -%02d ", -trebles[0]);
         }
         return text;
     }
 
     private String buildTrebleLine1RightResponse() {
         String text;
-        if (treble == minToneLevel) {
+        if (trebles[0] == minToneLevel) {
             text = "HF  MIN";
-        } else if (treble == maxToneLevel) {
+        } else if (trebles[0] == maxToneLevel) {
             text = "HF  MAX";
-        } else if (treble == 0) {
+        } else if (trebles[0] == 0) {
             text = "HF    0";
-        } else if (treble > 0) {
-            text = String.format("HF + %02d", treble);
+        } else if (trebles[0] > 0) {
+            text = String.format("HF + %02d", trebles[0]);
         } else {
-            text = String.format("HF - %02d", -treble);
+            text = String.format("HF - %02d", -trebles[0]);
         }
         return text;
     }
index fb5a82906b998964508dd3857815faf0b6ec15dd..f6f42867f5aa8c7d0daf9745793aea0cb60313ae 100644 (file)
@@ -301,14 +301,23 @@ public enum RotelSource {
     CAT20_BLUETOOTH(20, "BLUETOOTH", "Bluetooth", RotelCommand.SOURCE_BLUETOOTH),
     CAT20_XLR1(20, "XLR1", "XLR 1", RotelCommand.SOURCE_XLR1),
     CAT20_XLR2(20, "XLR2", "XLR 2", RotelCommand.SOURCE_XLR1),
-    CAT20_PCUSB(20, "PCUSB", "PC USB", RotelCommand.SOURCE_PCUSB);
+    CAT20_PCUSB(20, "PCUSB", "PC USB", RotelCommand.SOURCE_PCUSB),
+
+    CAT21_INPUTA(21, "INPUTA", "Input A", RotelCommand.SOURCE_INPUT_A, null, RotelCommand.ZONE1_SOURCE_INPUT_A,
+            RotelCommand.ZONE2_SOURCE_INPUT_A, RotelCommand.ZONE3_SOURCE_INPUT_A, RotelCommand.ZONE4_SOURCE_INPUT_A),
+    CAT21_INPUTB(21, "INPUTB", "Input B", RotelCommand.SOURCE_INPUT_B, null, RotelCommand.ZONE1_SOURCE_INPUT_B,
+            RotelCommand.ZONE2_SOURCE_INPUT_B, RotelCommand.ZONE3_SOURCE_INPUT_B, RotelCommand.ZONE4_SOURCE_INPUT_B),
+    CAT21_INPUTC(21, "INPUTC", "Input C", RotelCommand.SOURCE_INPUT_C, null, RotelCommand.ZONE1_SOURCE_INPUT_C,
+            RotelCommand.ZONE2_SOURCE_INPUT_C, RotelCommand.ZONE3_SOURCE_INPUT_C, RotelCommand.ZONE4_SOURCE_INPUT_C),
+    CAT21_INPUTD(21, "INPUTD", "Input D", RotelCommand.SOURCE_INPUT_D, null, RotelCommand.ZONE1_SOURCE_INPUT_D,
+            RotelCommand.ZONE2_SOURCE_INPUT_D, RotelCommand.ZONE3_SOURCE_INPUT_D, RotelCommand.ZONE4_SOURCE_INPUT_D);
 
     private int category;
     private String name;
     private String label;
     private @Nullable RotelCommand command;
     private @Nullable RotelCommand recordCommand;
-    private @Nullable RotelCommand mainZoneCommand;
+    private @Nullable RotelCommand zone1Command;
     private @Nullable RotelCommand zone2Command;
     private @Nullable RotelCommand zone3Command;
     private @Nullable RotelCommand zone4Command;
@@ -333,13 +342,13 @@ public enum RotelSource {
      * @param label the label of the source
      * @param command the command to select the source
      * @param recordCommand the command to select the source as source to be recorded
-     * @param mainZoneCommand the command to select the source in the main zone
+     * @param zone1Command the command to select the source in the zone 1 or main zone
      * @param zone2Command the command to select the source in the zone 2
      * @param zone3Command the command to select the source in the zone 3
      * @param zone4Command the command to select the source in the zone 4
      */
     private RotelSource(int category, String name, String label, @Nullable RotelCommand command,
-            @Nullable RotelCommand recordCommand, @Nullable RotelCommand mainZoneCommand,
+            @Nullable RotelCommand recordCommand, @Nullable RotelCommand zone1Command,
             @Nullable RotelCommand zone2Command, @Nullable RotelCommand zone3Command,
             @Nullable RotelCommand zone4Command) {
         this.category = category;
@@ -347,7 +356,7 @@ public enum RotelSource {
         this.label = label;
         this.command = command;
         this.recordCommand = recordCommand;
-        this.mainZoneCommand = mainZoneCommand;
+        this.zone1Command = zone1Command;
         this.zone2Command = zone2Command;
         this.zone3Command = zone3Command;
         this.zone4Command = zone4Command;
@@ -399,47 +408,33 @@ public enum RotelSource {
     }
 
     /**
-     * Get the command to select the source in the main zone
+     * Get the command to select the source in a zone
      *
-     * @return the command
-     */
-    public @Nullable RotelCommand getMainZoneCommand() {
-        return mainZoneCommand;
-    }
-
-    /**
-     * Get the command to select the source in the zone 2
+     * @param numZone the zone number, 1 for main zone or zone 1, 2 for zone 2, 3 for zone 3, 4 for zone 4
      *
      * @return the command
      */
-    public @Nullable RotelCommand getZone2Command() {
-        return zone2Command;
-    }
-
-    /**
-     * Get the command to select the source in the zone 3
-     *
-     * @return the command
-     */
-    public @Nullable RotelCommand getZone3Command() {
-        return zone3Command;
-    }
-
-    /**
-     * Get the command to select the source in the zone 4
-     *
-     * @return the command
-     */
-    public @Nullable RotelCommand getZone4Command() {
-        return zone4Command;
+    public @Nullable RotelCommand getZoneCommand(int numZone) {
+        switch (numZone) {
+            case 1:
+                return zone1Command;
+            case 2:
+                return zone2Command;
+            case 3:
+                return zone3Command;
+            case 4:
+                return zone4Command;
+            default:
+                throw new IllegalArgumentException("numZone must be a value between 1 and 4");
+        }
     }
 
     /**
      * Get the list of {@link RotelSource} available for a particular category of models
      *
      * @param category a category of models
-     * @param type a source type (0 for global source, 1 for main zone, 2 for zone 2, 3 for zone 3, 4 for zone 4 and 5
-     *            for record source)
+     * @param type a source type (0 for global source, 1 for main zone or zone 1, 2 for zone 2, 3 for zone 3, 4 for zone
+     *            4 and 5 for record source)
      *
      * @return the list of {@link RotelSource} available in a zone for a provided category of models
      */
@@ -447,9 +442,8 @@ public enum RotelSource {
         List<RotelSource> sources = new ArrayList<>();
         for (RotelSource value : RotelSource.values()) {
             if (value.getCategory() == category && ((type == 0 && value.getCommand() != null)
-                    || (type == 1 && value.getMainZoneCommand() != null)
-                    || (type == 2 && value.getZone2Command() != null) || (type == 3 && value.getZone3Command() != null)
-                    || (type == 4 && value.getZone4Command() != null)
+                    || (type == 1 && value.getZoneCommand(1) != null) || (type == 2 && value.getZoneCommand(2) != null)
+                    || (type == 3 && value.getZoneCommand(3) != null) || (type == 4 && value.getZoneCommand(4) != null)
                     || (type == 5 && value.getRecordCommand() != null))) {
                 sources.add(value);
             }
@@ -481,8 +475,8 @@ public enum RotelSource {
      *
      * @param category a category of models
      * @param command the command used to identify the source
-     * @param type a source type (0 for global source, 1 for main zone, 2 for zone 2, 3 for zone 3, 4 for zone 4 and 5
-     *            for record source)
+     * @param type a source type (0 for global source, 1 for main zone or zone 1, 2 for zone 2, 3 for zone 3,
+     *            4 for zone 4 and 5 for record source)
      *
      * @return the source associated to the searched command for the provided category of models
      *
@@ -491,10 +485,10 @@ public enum RotelSource {
     public static RotelSource getFromCommand(int category, RotelCommand command, int type) throws RotelException {
         for (RotelSource value : RotelSource.values()) {
             if (value.getCategory() == category && ((type == 0 && value.getCommand() == command)
-                    || (type == 1 && value.getMainZoneCommand() == command)
-                    || (type == 2 && value.getZone2Command() == command)
-                    || (type == 3 && value.getZone3Command() == command)
-                    || (type == 4 && value.getZone4Command() == command)
+                    || (type == 1 && value.getZoneCommand(1) == command)
+                    || (type == 2 && value.getZoneCommand(2) == command)
+                    || (type == 3 && value.getZoneCommand(3) == command)
+                    || (type == 4 && value.getZoneCommand(4) == command)
                     || (type == 5 && value.getRecordCommand() == command))) {
                 return value;
             }
index 39876a3d30d77d7f883eb792b30d4b004bfc72e3..6a2d2392b4ed2670811f023f01bb6888dc6d1c82 100644 (file)
@@ -82,11 +82,8 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     private static final int SLEEP_INTV = 30;
 
     private @Nullable ScheduledFuture<?> reconnectJob;
-    private @Nullable ScheduledFuture<?> powerOnJob;
     private @Nullable ScheduledFuture<?> powerOffJob;
-    private @Nullable ScheduledFuture<?> powerOnZone2Job;
-    private @Nullable ScheduledFuture<?> powerOnZone3Job;
-    private @Nullable ScheduledFuture<?> powerOnZone4Job;
+    private @Nullable ScheduledFuture<?>[] powerOnZoneJobs = { null, null, null, null, null };
 
     private RotelStateDescriptionOptionProvider stateDescriptionProvider;
     private SerialPortManager serialPortManager;
@@ -103,37 +100,24 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
 
     private int currentZone = 1;
     private boolean selectingRecord;
-    private @Nullable Boolean power;
-    private boolean powerZone2;
-    private boolean powerZone3;
-    private boolean powerZone4;
-    private RotelSource source = RotelSource.CAT0_CD;
+    private @Nullable Boolean[] powers = { null, false, false, false, false };
+    private boolean powerControlPerZone;
     private @Nullable RotelSource recordSource;
-    private @Nullable RotelSource sourceZone2;
-    private @Nullable RotelSource sourceZone3;
-    private @Nullable RotelSource sourceZone4;
+    private @Nullable RotelSource[] sources = { RotelSource.CAT0_CD, null, null, null, null };
     private RotelDsp dsp = RotelDsp.CAT1_NONE;
-    private int volume;
-    private boolean mute;
-    private boolean fixedVolumeZone2;
-    private int volumeZone2;
-    private boolean muteZone2;
-    private boolean fixedVolumeZone3;
-    private int volumeZone3;
-    private boolean muteZone3;
-    private boolean fixedVolumeZone4;
-    private int volumeZone4;
-    private boolean muteZone4;
-    private int bass;
-    private int treble;
+    private boolean[] fixedVolumeZones = { false, false, false, false, false };
+    private int[] volumes = { 0, 0, 0, 0, 0 };
+    private boolean[] mutes = { false, false, false, false, false };
+    private int[] basses = { 0, 0, 0, 0, 0 };
+    private int[] trebles = { 0, 0, 0, 0, 0 };
     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
     private int track;
-    private double frequency;
+    private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
     private String frontPanelLine1 = "";
     private String frontPanelLine2 = "";
     private int brightness;
     private boolean tcbypass;
-    private int balance;
+    private int[] balances = { 0, 0, 0, 0, 0 };
     private int minBalanceLevel;
     private int maxBalanceLevel;
     private boolean speakera;
@@ -281,6 +265,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             case THING_TYPE_ID_T14:
                 model = RotelModel.T14;
                 break;
+            case THING_TYPE_ID_C8:
+                model = RotelModel.C8;
+                break;
             case THING_TYPE_ID_M8:
                 model = RotelModel.M8;
                 break;
@@ -350,6 +337,8 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     minBalanceLevel, maxBalanceLevel);
         }
 
+        powerControlPerZone = model.hasPowerControlPerZone();
+
         // Check configuration settings
         String configError = null;
         if ((config.serialPort == null || config.serialPort.isEmpty())
@@ -445,17 +434,21 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
                         getStateOptions(model.getRecordSources(), sourcesCustomLabels));
             }
-            if (model.hasZone2SourceControl()) {
+            if (model.hasZoneSourceControl(1)) {
+                stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
+                        getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
+            }
+            if (model.hasZoneSourceControl(2)) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
-                        getStateOptions(model.getZone2Sources(), sourcesCustomLabels));
+                        getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
             }
-            if (model.hasZone3SourceControl()) {
+            if (model.hasZoneSourceControl(3)) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
-                        getStateOptions(model.getZone3Sources(), sourcesCustomLabels));
+                        getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
             }
-            if (model.hasZone4SourceControl()) {
+            if (model.hasZoneSourceControl(4)) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
-                        getStateOptions(model.getZone4Sources(), sourcesCustomLabels));
+                        getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
             }
             if (model.hasDspControl()) {
                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
@@ -476,10 +469,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     public void dispose() {
         logger.debug("Disposing handler for thing {}", getThing().getUID());
         cancelPowerOffJob();
-        cancelPowerOnJob();
-        cancelPowerOnZone2Job();
-        cancelPowerOnZone3Job();
-        cancelPowerOnZone4Job();
+        for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
+            cancelPowerOnZoneJob(zone);
+        }
         cancelReconnectJob();
         closeConnection();
         super.dispose();
@@ -513,6 +505,48 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             return;
         }
 
+        int numZone = 0;
+        switch (channel) {
+            case CHANNEL_ZONE1_SOURCE:
+            case CHANNEL_ZONE1_VOLUME:
+            case CHANNEL_ZONE1_MUTE:
+            case CHANNEL_ZONE1_BASS:
+            case CHANNEL_ZONE1_TREBLE:
+            case CHANNEL_ZONE1_BALANCE:
+                numZone = 1;
+                break;
+            case CHANNEL_ZONE2_POWER:
+            case CHANNEL_ZONE2_SOURCE:
+            case CHANNEL_ZONE2_VOLUME:
+            case CHANNEL_ZONE2_VOLUME_UP_DOWN:
+            case CHANNEL_ZONE2_MUTE:
+            case CHANNEL_ZONE2_BASS:
+            case CHANNEL_ZONE2_TREBLE:
+            case CHANNEL_ZONE2_BALANCE:
+                numZone = 2;
+                break;
+            case CHANNEL_ZONE3_POWER:
+            case CHANNEL_ZONE3_SOURCE:
+            case CHANNEL_ZONE3_VOLUME:
+            case CHANNEL_ZONE3_MUTE:
+            case CHANNEL_ZONE3_BASS:
+            case CHANNEL_ZONE3_TREBLE:
+            case CHANNEL_ZONE3_BALANCE:
+                numZone = 3;
+                break;
+            case CHANNEL_ZONE4_POWER:
+            case CHANNEL_ZONE4_SOURCE:
+            case CHANNEL_ZONE4_VOLUME:
+            case CHANNEL_ZONE4_MUTE:
+            case CHANNEL_ZONE4_BASS:
+            case CHANNEL_ZONE4_TREBLE:
+            case CHANNEL_ZONE4_BALANCE:
+                numZone = 4;
+                break;
+            default:
+                break;
+        }
+
         RotelSource src;
         RotelCommand cmd;
         boolean success = true;
@@ -521,13 +555,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 switch (channel) {
                     case CHANNEL_POWER:
                     case CHANNEL_MAIN_POWER:
-                        handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
-                        break;
                     case CHANNEL_ZONE2_POWER:
-                        if (model.hasZone2Commands()) {
-                            handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
-                        } else if (model.getNbAdditionalZones() == 1) {
-                            if (isPowerOn() || powerZone2) {
+                    case CHANNEL_ZONE3_POWER:
+                    case CHANNEL_ZONE4_POWER:
+                        if (numZone == 0 || model.hasZoneCommands(numZone)) {
+                            handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
+                        } else if (numZone == 2 && model.getNumberOfZones() == 2) {
+                            if (isPowerOn() || isPowerOn(numZone)) {
                                 selectZone(2, model.getZoneSelectCmd());
                             }
                             sendCommand(RotelCommand.ZONE_SELECT);
@@ -536,30 +570,26 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         }
                         break;
-                    case CHANNEL_ZONE3_POWER:
-                        if (model.hasZone3Commands()) {
-                            handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
-                    case CHANNEL_ZONE4_POWER:
-                        if (model.hasZone4Commands()) {
-                            handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
+                    case CHANNEL_ALL_POWER:
+                        handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
                         break;
                     case CHANNEL_SOURCE:
                     case CHANNEL_MAIN_SOURCE:
-                        if (!isPowerOn()) {
+                    case CHANNEL_ZONE1_SOURCE:
+                    case CHANNEL_ZONE2_SOURCE:
+                    case CHANNEL_ZONE3_SOURCE:
+                    case CHANNEL_ZONE4_SOURCE:
+                        if (!isPowerOn(numZone)) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else {
+                            logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
+                                    numZone == 0 ? "device" : "zone " + numZone);
+                        } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
                             src = model.getSourceFromName(command.toString());
-                            cmd = model.hasOtherThanPrimaryCommands() ? src.getMainZoneCommand() : src.getCommand();
+                            if (numZone == 0) {
+                                cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
+                            } else {
+                                cmd = src.getZoneCommand(numZone);
+                            }
                             if (cmd != null) {
                                 sendCommand(cmd);
                                 if (model.canGetFrequency()) {
@@ -579,56 +609,24 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
                                         channel);
                             }
-                        }
-                        break;
-                    case CHANNEL_MAIN_RECORD_SOURCE:
-                        if (!isPowerOn()) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (model.hasOtherThanPrimaryCommands()) {
-                            src = model.getSourceFromName(command.toString());
-                            cmd = src.getRecordCommand();
-                            if (cmd != null) {
-                                sendCommand(cmd);
-                            } else {
-                                success = false;
-                                logger.debug("Command {} from channel {} failed: undefined record source command",
-                                        command, channel);
-                            }
-                        } else {
-                            src = model.getSourceFromName(command.toString());
-                            cmd = src.getCommand();
-                            if (cmd != null) {
-                                sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
-                                Thread.sleep(100);
-                                sendCommand(cmd);
-                            } else {
-                                success = false;
-                                logger.debug("Command {} from channel {} failed: undefined source command", command,
-                                        channel);
-                            }
-                        }
-                        break;
-                    case CHANNEL_ZONE2_SOURCE:
-                        if (!powerZone2) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
-                        } else if (model.hasZone2Commands()) {
-                            src = model.getSourceFromName(command.toString());
-                            cmd = src.getZone2Command();
-                            if (cmd != null) {
-                                sendCommand(cmd);
-                            } else {
-                                success = false;
-                                logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
-                                        command, channel);
-                            }
-                        } else if (model.getNbAdditionalZones() >= 1) {
+                        } else if (numZone == 2 && model.getNumberOfZones() > 1) {
                             src = model.getSourceFromName(command.toString());
                             cmd = src.getCommand();
                             if (cmd != null) {
                                 selectZone(2, model.getZoneSelectCmd());
                                 sendCommand(cmd);
+                                if (model.canGetFrequency()) {
+                                    // send <new-source> returns
+                                    // 1.) the selected <new-source>
+                                    // 2.) the used frequency
+                                    // BUT:
+                                    // 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);
+                                    sendCommand(RotelCommand.FREQUENCY);
+                                    Thread.sleep(100);
+                                    updateChannelState(CHANNEL_FREQUENCY);
+                                }
                             } else {
                                 success = false;
                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
@@ -639,42 +637,32 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         }
                         break;
-                    case CHANNEL_ZONE3_SOURCE:
-                        if (!powerZone3) {
+                    case CHANNEL_MAIN_RECORD_SOURCE:
+                        if (!isPowerOn()) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
-                        } else if (model.hasZone3Commands()) {
+                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
+                        } else if (model.hasOtherThanPrimaryCommands()) {
                             src = model.getSourceFromName(command.toString());
-                            cmd = src.getZone3Command();
+                            cmd = src.getRecordCommand();
                             if (cmd != null) {
                                 sendCommand(cmd);
                             } else {
                                 success = false;
-                                logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
+                                logger.debug("Command {} from channel {} failed: undefined record source command",
                                         command, channel);
                             }
                         } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
-                    case CHANNEL_ZONE4_SOURCE:
-                        if (!powerZone4) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
-                        } else if (model.hasZone4Commands()) {
                             src = model.getSourceFromName(command.toString());
-                            cmd = src.getZone4Command();
+                            cmd = src.getCommand();
                             if (cmd != null) {
+                                sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
+                                Thread.sleep(100);
                                 sendCommand(cmd);
                             } else {
                                 success = false;
-                                logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
-                                        command, channel);
+                                logger.debug("Command {} from channel {} failed: undefined source command", command,
+                                        channel);
                             }
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         }
                         break;
                     case CHANNEL_DSP:
@@ -688,100 +676,30 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         break;
                     case CHANNEL_VOLUME:
                     case CHANNEL_MAIN_VOLUME:
-                        if (!isPowerOn()) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (model.hasVolumeControl()) {
-                            handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
-                                    RotelCommand.VOLUME_SET);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
                     case CHANNEL_MAIN_VOLUME_UP_DOWN:
-                        if (!isPowerOn()) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } else if (model.hasVolumeControl()) {
-                            handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
-                                    null);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
+                    case CHANNEL_ZONE1_VOLUME:
                     case CHANNEL_ZONE2_VOLUME:
-                        if (!powerZone2) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
-                        } else if (fixedVolumeZone2) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
-                                    channel);
-                        } 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, model.getZoneSelectCmd());
-                                handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
-                                        RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
-                            }
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
                     case CHANNEL_ZONE2_VOLUME_UP_DOWN:
-                        if (!powerZone2) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
-                        } else if (fixedVolumeZone2) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
-                                    channel);
-                        } 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, model.getZoneSelectCmd());
-                                handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
-                                        RotelCommand.VOLUME_DOWN, null);
-                            }
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
                     case CHANNEL_ZONE3_VOLUME:
-                        if (!powerZone3) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
-                        } else if (fixedVolumeZone3) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
-                                    channel);
-                        } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
-                            handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
-                                    RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
                     case CHANNEL_ZONE4_VOLUME:
-                        if (!powerZone4) {
+                        if (!isPowerOn(numZone)) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
-                        } else if (fixedVolumeZone4) {
+                            logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
+                                    numZone == 0 ? "device" : "zone " + numZone);
+                        } else if (fixedVolumeZones[numZone]) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
-                                    channel);
-                        } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
-                            handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
-                                    RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
+                            logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
+                        } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
+                            handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
+                                    getVolumeDownCommand(numZone),
+                                    CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
+                                            || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
+                                                    : getVolumeSetCommand(numZone));
+                        } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
+                            selectZone(2, model.getZoneSelectCmd());
+                            handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
+                                    RotelCommand.VOLUME_DOWN,
+                                    CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
                         } else {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
@@ -789,48 +707,18 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         break;
                     case CHANNEL_MUTE:
                     case CHANNEL_MAIN_MUTE:
-                        if (!isPowerOn()) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
-                        } 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);
-                        }
-                        break;
+                    case CHANNEL_ZONE1_MUTE:
                     case CHANNEL_ZONE2_MUTE:
-                        if (!powerZone2) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
-                        } else if (model.hasVolumeControl() && model.hasZone2Commands()) {
-                            handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
-                                    RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
                     case CHANNEL_ZONE3_MUTE:
-                        if (!powerZone3) {
-                            success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
-                        } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
-                            handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
-                                    RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
-                        } else {
-                            success = false;
-                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
-                        }
-                        break;
                     case CHANNEL_ZONE4_MUTE:
-                        if (!powerZone4) {
+                        if (!isPowerOn(numZone)) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
-                        } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
-                            handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
-                                    RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
+                            logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
+                                    numZone == 0 ? "device" : "zone " + numZone);
+                        } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
+                            handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
+                                    getMuteOnCommand(numZone), getMuteOffCommand(numZone),
+                                    getMuteToggleCommand(numZone));
                         } else {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
@@ -838,30 +726,46 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         break;
                     case CHANNEL_BASS:
                     case CHANNEL_MAIN_BASS:
-                        if (!isPowerOn()) {
+                    case CHANNEL_ZONE1_BASS:
+                    case CHANNEL_ZONE2_BASS:
+                    case CHANNEL_ZONE3_BASS:
+                    case CHANNEL_ZONE4_BASS:
+                        if (!isPowerOn(numZone)) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
+                            logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
+                                    numZone == 0 ? "device" : "zone " + numZone);
                         } else if (tcbypass) {
+                            success = false;
                             logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
                                     channel);
-                            updateChannelState(CHANNEL_BASS);
+                        } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
+                            handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
+                                    getBassDownCommand(numZone), getBassSetCommand(numZone));
                         } else {
-                            handleToneCmd(bass, channel, command, 2, RotelCommand.BASS_UP, RotelCommand.BASS_DOWN,
-                                    RotelCommand.BASS_SET);
+                            success = false;
+                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         }
                         break;
                     case CHANNEL_TREBLE:
                     case CHANNEL_MAIN_TREBLE:
-                        if (!isPowerOn()) {
+                    case CHANNEL_ZONE1_TREBLE:
+                    case CHANNEL_ZONE2_TREBLE:
+                    case CHANNEL_ZONE3_TREBLE:
+                    case CHANNEL_ZONE4_TREBLE:
+                        if (!isPowerOn(numZone)) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
+                            logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
+                                    numZone == 0 ? "device" : "zone " + numZone);
                         } else if (tcbypass) {
+                            success = false;
                             logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
                                     channel);
-                            updateChannelState(CHANNEL_TREBLE);
+                        } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
+                            handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
+                                    getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
                         } else {
-                            handleToneCmd(treble, channel, command, 1, RotelCommand.TREBLE_UP, RotelCommand.TREBLE_DOWN,
-                                    RotelCommand.TREBLE_SET);
+                            success = false;
+                            logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         }
                         break;
                     case CHANNEL_PLAY_CONTROL:
@@ -887,6 +791,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         }
                         break;
                     case CHANNEL_BRIGHTNESS:
+                    case CHANNEL_ALL_BRIGHTNESS:
                         if (!isPowerOn()) {
                             success = false;
                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
@@ -919,15 +824,20 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         }
                         break;
                     case CHANNEL_BALANCE:
-                        if (!isPowerOn()) {
+                    case CHANNEL_ZONE1_BALANCE:
+                    case CHANNEL_ZONE2_BALANCE:
+                    case CHANNEL_ZONE3_BALANCE:
+                    case CHANNEL_ZONE4_BALANCE:
+                        if (!isPowerOn(numZone)) {
                             success = false;
-                            logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
+                            logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
+                                    numZone == 0 ? "device" : "zone " + numZone);
                         } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
                             success = false;
                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
                         } else {
-                            handleBalanceCmd(channel, command, RotelCommand.BALANCE_LEFT, RotelCommand.BALANCE_RIGHT,
-                                    RotelCommand.BALANCE_SET);
+                            handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
+                                    getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
                         }
                         break;
                     case CHANNEL_SPEAKER_A:
@@ -1110,8 +1020,8 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
         if (command instanceof OnOffType) {
             if (command == OnOffType.ON) {
                 sendCommand(onCmd);
-                bass = 0;
-                treble = 0;
+                basses[0] = 0;
+                trebles[0] = 0;
                 updateChannelState(CHANNEL_BASS);
                 updateChannelState(CHANNEL_TREBLE);
             } else if (command == OnOffType.OFF) {
@@ -1207,11 +1117,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      */
     private void selectZone(int zone, @Nullable RotelCommand selectCommand)
             throws RotelException, InterruptedException {
-        if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1 && zone >= 1 && zone != currentZone
+        if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
                 && selectCommand != null) {
             int nbSelect;
             if (zone < currentZone) {
-                nbSelect = zone + model.getNbAdditionalZones() - currentZone;
+                nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
                 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
                     nbSelect++;
                 }
@@ -1287,6 +1197,53 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
         if (!KEY_ERROR.equals(key)) {
             updateStatus(ThingStatus.ONLINE);
         }
+        int numZone = 0;
+        switch (key) {
+            case KEY_INPUT_ZONE1:
+            case KEY_VOLUME_ZONE1:
+            case KEY_MUTE_ZONE1:
+            case KEY_BASS_ZONE1:
+            case KEY_TREBLE_ZONE1:
+            case KEY_BALANCE_ZONE1:
+            case KEY_FREQ_ZONE1:
+                numZone = 1;
+                break;
+            case KEY_POWER_ZONE2:
+            case KEY_SOURCE_ZONE2:
+            case KEY_INPUT_ZONE2:
+            case KEY_VOLUME_ZONE2:
+            case KEY_MUTE_ZONE2:
+            case KEY_BASS_ZONE2:
+            case KEY_TREBLE_ZONE2:
+            case KEY_BALANCE_ZONE2:
+            case KEY_FREQ_ZONE2:
+                numZone = 2;
+                break;
+            case KEY_POWER_ZONE3:
+            case KEY_SOURCE_ZONE3:
+            case KEY_INPUT_ZONE3:
+            case KEY_VOLUME_ZONE3:
+            case KEY_MUTE_ZONE3:
+            case KEY_BASS_ZONE3:
+            case KEY_TREBLE_ZONE3:
+            case KEY_BALANCE_ZONE3:
+            case KEY_FREQ_ZONE3:
+                numZone = 3;
+                break;
+            case KEY_POWER_ZONE4:
+            case KEY_SOURCE_ZONE4:
+            case KEY_INPUT_ZONE4:
+            case KEY_VOLUME_ZONE4:
+            case KEY_MUTE_ZONE4:
+            case KEY_BASS_ZONE4:
+            case KEY_TREBLE_ZONE4:
+            case KEY_BALANCE_ZONE4:
+            case KEY_FREQ_ZONE4:
+                numZone = 4;
+                break;
+            default:
+                break;
+        }
         try {
             switch (key) {
                 case KEY_ERROR:
@@ -1314,6 +1271,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         handlePowerOn();
                     } else if (STANDBY.equalsIgnoreCase(value)) {
                         handlePowerOff();
+                        if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
+                            for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                                handlePowerOffZone(zone);
+                            }
+                        }
                     } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
                         schedulePowerOffJob(false);
                     } else {
@@ -1321,28 +1283,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     }
                     break;
                 case KEY_POWER_ZONE2:
-                    if (POWER_ON.equalsIgnoreCase(value)) {
-                        handlePowerOnZone2();
-                    } else if (STANDBY.equalsIgnoreCase(value)) {
-                        handlePowerOffZone2();
-                    } else {
-                        throw new RotelException("Invalid value");
-                    }
-                    break;
                 case KEY_POWER_ZONE3:
-                    if (POWER_ON.equalsIgnoreCase(value)) {
-                        handlePowerOnZone3();
-                    } else if (STANDBY.equalsIgnoreCase(value)) {
-                        handlePowerOffZone3();
-                    } else {
-                        throw new RotelException("Invalid value");
-                    }
-                    break;
                 case KEY_POWER_ZONE4:
                     if (POWER_ON.equalsIgnoreCase(value)) {
-                        handlePowerOnZone4();
+                        handlePowerOnZone(numZone);
                     } else if (STANDBY.equalsIgnoreCase(value)) {
-                        handlePowerOffZone4();
+                        handlePowerOffZone(numZone);
                     } else {
                         throw new RotelException("Invalid value");
                     }
@@ -1362,134 +1308,101 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     }
                     break;
                 case KEY_VOLUME:
-                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        volume = minVolume;
+                case KEY_VOLUME_ZONE1:
+                case KEY_VOLUME_ZONE2:
+                case KEY_VOLUME_ZONE3:
+                case KEY_VOLUME_ZONE4:
+                    fixedVolumeZones[numZone] = false;
+                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
+                        fixedVolumeZones[numZone] = true;
+                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                        volumes[numZone] = minVolume;
                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        volume = maxVolume;
+                        volumes[numZone] = maxVolume;
                     } else {
-                        volume = Integer.parseInt(value);
+                        volumes[numZone] = Integer.parseInt(value);
+                    }
+                    if (numZone == 0) {
+                        updateChannelState(CHANNEL_VOLUME);
+                        updateChannelState(CHANNEL_MAIN_VOLUME);
+                        updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
+                    } else {
+                        updateGroupChannelState(numZone, CHANNEL_VOLUME);
+                        updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
                     }
-                    updateChannelState(CHANNEL_VOLUME);
-                    updateChannelState(CHANNEL_MAIN_VOLUME);
-                    updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
                     break;
                 case KEY_MUTE:
+                case KEY_MUTE_ZONE1:
+                case KEY_MUTE_ZONE2:
+                case KEY_MUTE_ZONE3:
+                case KEY_MUTE_ZONE4:
                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                        mute = true;
-                        updateChannelState(CHANNEL_MUTE);
-                        updateChannelState(CHANNEL_MAIN_MUTE);
+                        mutes[numZone] = true;
+                        if (numZone == 0) {
+                            updateChannelState(CHANNEL_MUTE);
+                            updateChannelState(CHANNEL_MAIN_MUTE);
+                        } else {
+                            updateGroupChannelState(numZone, CHANNEL_MUTE);
+                        }
                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-                        mute = false;
-                        updateChannelState(CHANNEL_MUTE);
-                        updateChannelState(CHANNEL_MAIN_MUTE);
+                        mutes[numZone] = false;
+                        if (numZone == 0) {
+                            updateChannelState(CHANNEL_MUTE);
+                            updateChannelState(CHANNEL_MAIN_MUTE);
+                        } else {
+                            updateGroupChannelState(numZone, CHANNEL_MUTE);
+                        }
                     } else {
                         throw new RotelException("Invalid value");
                     }
                     break;
-                case KEY_VOLUME_ZONE2:
-                    fixedVolumeZone2 = false;
-                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
-                        fixedVolumeZone2 = true;
-                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        volumeZone2 = minVolume;
+                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 KEY_BASS:
+                case KEY_BASS_ZONE1:
+                case KEY_BASS_ZONE2:
+                case KEY_BASS_ZONE3:
+                case KEY_BASS_ZONE4:
+                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                        basses[numZone] = minToneLevel;
                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        volumeZone2 = maxVolume;
+                        basses[numZone] = maxToneLevel;
                     } else {
-                        volumeZone2 = Integer.parseInt(value);
+                        basses[numZone] = Integer.parseInt(value);
                     }
-                    updateChannelState(CHANNEL_ZONE2_VOLUME);
-                    updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
-                    break;
-                case KEY_VOLUME_ZONE3:
-                    fixedVolumeZone3 = false;
-                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
-                        fixedVolumeZone3 = true;
-                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        volumeZone3 = minVolume;
-                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        volumeZone3 = maxVolume;
+                    if (numZone == 0) {
+                        updateChannelState(CHANNEL_BASS);
+                        updateChannelState(CHANNEL_MAIN_BASS);
                     } else {
-                        volumeZone3 = Integer.parseInt(value);
+                        updateGroupChannelState(numZone, CHANNEL_BASS);
                     }
-                    updateChannelState(CHANNEL_ZONE3_VOLUME);
                     break;
-                case KEY_VOLUME_ZONE4:
-                    fixedVolumeZone4 = false;
-                    if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
-                        fixedVolumeZone4 = true;
-                    } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        volumeZone4 = minVolume;
+                case KEY_TREBLE:
+                case KEY_TREBLE_ZONE1:
+                case KEY_TREBLE_ZONE2:
+                case KEY_TREBLE_ZONE3:
+                case KEY_TREBLE_ZONE4:
+                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
+                        trebles[numZone] = minToneLevel;
                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        volumeZone4 = maxVolume;
+                        trebles[numZone] = maxToneLevel;
                     } else {
-                        volumeZone4 = Integer.parseInt(value);
+                        trebles[numZone] = Integer.parseInt(value);
                     }
-                    updateChannelState(CHANNEL_ZONE4_VOLUME);
-                    break;
-                case KEY_MUTE_ZONE2:
-                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                        muteZone2 = true;
-                        updateChannelState(CHANNEL_ZONE2_MUTE);
-                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-                        muteZone2 = false;
-                        updateChannelState(CHANNEL_ZONE2_MUTE);
+                    if (numZone == 0) {
+                        updateChannelState(CHANNEL_TREBLE);
+                        updateChannelState(CHANNEL_MAIN_TREBLE);
                     } else {
-                        throw new RotelException("Invalid value");
-                    }
-                    break;
-                case KEY_MUTE_ZONE3:
-                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                        muteZone3 = true;
-                        updateChannelState(CHANNEL_ZONE3_MUTE);
-                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-                        muteZone3 = false;
-                        updateChannelState(CHANNEL_ZONE3_MUTE);
-                    } else {
-                        throw new RotelException("Invalid value");
+                        updateGroupChannelState(numZone, CHANNEL_TREBLE);
                     }
                     break;
-                case KEY_MUTE_ZONE4:
-                    if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
-                        muteZone4 = true;
-                        updateChannelState(CHANNEL_ZONE4_MUTE);
-                    } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-                        muteZone4 = false;
-                        updateChannelState(CHANNEL_ZONE4_MUTE);
-                    } else {
-                        throw new RotelException("Invalid value");
-                    }
-                    break;
-                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 KEY_BASS:
-                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        bass = minToneLevel;
-                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        bass = maxToneLevel;
-                    } else {
-                        bass = Integer.parseInt(value);
-                    }
-                    updateChannelState(CHANNEL_BASS);
-                    updateChannelState(CHANNEL_MAIN_BASS);
-                    break;
-                case KEY_TREBLE:
-                    if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        treble = minToneLevel;
-                    } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        treble = maxToneLevel;
-                    } else {
-                        treble = Integer.parseInt(value);
-                    }
-                    updateChannelState(CHANNEL_TREBLE);
-                    updateChannelState(CHANNEL_MAIN_TREBLE);
-                    break;
                 case KEY_SOURCE:
-                    source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
+                    sources[0] = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
                     updateChannelState(CHANNEL_SOURCE);
                     updateChannelState(CHANNEL_MAIN_SOURCE);
                     break;
@@ -1498,16 +1411,14 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
                     break;
                 case KEY_SOURCE_ZONE2:
-                    sourceZone2 = model.getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
-                    updateChannelState(CHANNEL_ZONE2_SOURCE);
-                    break;
                 case KEY_SOURCE_ZONE3:
-                    sourceZone3 = model.getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
-                    updateChannelState(CHANNEL_ZONE3_SOURCE);
-                    break;
                 case KEY_SOURCE_ZONE4:
-                    sourceZone4 = model.getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
-                    updateChannelState(CHANNEL_ZONE4_SOURCE);
+                case KEY_INPUT_ZONE1:
+                case KEY_INPUT_ZONE2:
+                case KEY_INPUT_ZONE3:
+                case KEY_INPUT_ZONE4:
+                    sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
+                    updateGroupChannelState(numZone, CHANNEL_SOURCE);
                     break;
                 case KEY_DSP_MODE:
                     if ("dolby_pliix_movie".equals(value)) {
@@ -1538,26 +1449,36 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     }
                     break;
                 case KEY_TRACK:
-                    if (source.getName().equals("CD") && !model.hasSourceControl()) {
+                    RotelSource source = sources[0];
+                    if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
                         track = Integer.parseInt(value);
                         updateChannelState(CHANNEL_TRACK);
                     }
                     break;
                 case KEY_FREQ:
-                    if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
-                        frequency = 0.0;
+                case KEY_FREQ_ZONE1:
+                case KEY_FREQ_ZONE2:
+                case KEY_FREQ_ZONE3:
+                case KEY_FREQ_ZONE4:
+                    if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
+                        frequencies[numZone] = 0.0;
                     } else {
                         // Suppress a potential ending "k" or "K"
                         if (value.toUpperCase().endsWith("K")) {
                             value = value.substring(0, value.length() - 1);
                         }
-                        frequency = Double.parseDouble(value);
+                        frequencies[numZone] = Double.parseDouble(value);
+                    }
+                    if (numZone == 0) {
+                        updateChannelState(CHANNEL_FREQUENCY);
+                    } else {
+                        updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
                     }
-                    updateChannelState(CHANNEL_FREQUENCY);
                     break;
                 case KEY_DIMMER:
                     brightness = Integer.parseInt(value);
                     updateChannelState(CHANNEL_BRIGHTNESS);
+                    updateChannelState(CHANNEL_ALL_BRIGHTNESS);
                     break;
                 case KEY_UPDATE_MODE:
                 case KEY_DISPLAY_UPDATE:
@@ -1585,18 +1506,26 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                     }
                     break;
                 case KEY_BALANCE:
+                case KEY_BALANCE_ZONE1:
+                case KEY_BALANCE_ZONE2:
+                case KEY_BALANCE_ZONE3:
+                case KEY_BALANCE_ZONE4:
                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
-                        balance = minBalanceLevel;
+                        balances[numZone] = minBalanceLevel;
                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
-                        balance = maxBalanceLevel;
+                        balances[numZone] = maxBalanceLevel;
                     } else if (value.toUpperCase().startsWith("L")) {
-                        balance = -Integer.parseInt(value.substring(1));
+                        balances[numZone] = -Integer.parseInt(value.substring(1));
                     } else if (value.toUpperCase().startsWith("R")) {
-                        balance = Integer.parseInt(value.substring(1));
+                        balances[numZone] = Integer.parseInt(value.substring(1));
                     } else {
-                        balance = Integer.parseInt(value);
+                        balances[numZone] = Integer.parseInt(value);
+                    }
+                    if (numZone == 0) {
+                        updateChannelState(CHANNEL_BALANCE);
+                    } else {
+                        updateGroupChannelState(numZone, CHANNEL_BALANCE);
                     }
-                    updateChannelState(CHANNEL_BALANCE);
                     break;
                 case KEY_SPEAKER:
                     if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
@@ -1623,6 +1552,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                         throw new RotelException("Invalid value");
                     }
                     break;
+                case KEY_MODEL:
+                    getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
+                    break;
+                case KEY_VERSION:
+                    getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
+                    break;
                 default:
                     logger.debug("onNewMessageEvent: unhandled key {}", key);
                     break;
@@ -1636,10 +1571,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * Handle the received information that device power (main zone) is ON
      */
     private void handlePowerOn() {
-        Boolean prev = power;
-        power = true;
+        Boolean prev = powers[0];
+        powers[0] = true;
         updateChannelState(CHANNEL_POWER);
         updateChannelState(CHANNEL_MAIN_POWER);
+        updateChannelState(CHANNEL_ALL_POWER);
         if ((prev == null) || !prev) {
             schedulePowerOnJob();
         }
@@ -1649,24 +1585,15 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      * Handle the received information that device power (main zone) is OFF
      */
     private void handlePowerOff() {
-        cancelPowerOnJob();
-        power = false;
+        cancelPowerOnZoneJob(0);
+        powers[0] = false;
         updateChannelState(CHANNEL_POWER);
-        updateChannelState(CHANNEL_MAIN_POWER);
         updateChannelState(CHANNEL_SOURCE);
-        updateChannelState(CHANNEL_MAIN_SOURCE);
-        updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
         updateChannelState(CHANNEL_DSP);
-        updateChannelState(CHANNEL_MAIN_DSP);
         updateChannelState(CHANNEL_VOLUME);
-        updateChannelState(CHANNEL_MAIN_VOLUME);
-        updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
         updateChannelState(CHANNEL_MUTE);
-        updateChannelState(CHANNEL_MAIN_MUTE);
         updateChannelState(CHANNEL_BASS);
-        updateChannelState(CHANNEL_MAIN_BASS);
         updateChannelState(CHANNEL_TREBLE);
-        updateChannelState(CHANNEL_MAIN_TREBLE);
         updateChannelState(CHANNEL_PLAY_CONTROL);
         updateChannelState(CHANNEL_TRACK);
         updateChannelState(CHANNEL_FREQUENCY);
@@ -1675,79 +1602,48 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
         updateChannelState(CHANNEL_BALANCE);
         updateChannelState(CHANNEL_SPEAKER_A);
         updateChannelState(CHANNEL_SPEAKER_B);
-    }
-
-    /**
-     * Handle the received information that zone 2 power is ON
-     */
-    private void handlePowerOnZone2() {
-        boolean prev = powerZone2;
-        powerZone2 = true;
-        updateChannelState(CHANNEL_ZONE2_POWER);
-        if (!prev) {
-            schedulePowerOnZone2Job();
-        }
-    }
 
-    /**
-     * Handle the received information that zone 2 power is OFF
-     */
-    private void handlePowerOffZone2() {
-        cancelPowerOnZone2Job();
-        powerZone2 = false;
-        updateChannelState(CHANNEL_ZONE2_POWER);
-        updateChannelState(CHANNEL_ZONE2_SOURCE);
-        updateChannelState(CHANNEL_ZONE2_VOLUME);
-        updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
-        updateChannelState(CHANNEL_ZONE2_MUTE);
-    }
-
-    /**
-     * Handle the received information that zone 3 power is ON
-     */
-    private void handlePowerOnZone3() {
-        boolean prev = powerZone3;
-        powerZone3 = true;
-        updateChannelState(CHANNEL_ZONE3_POWER);
-        if (!prev) {
-            schedulePowerOnZone3Job();
-        }
-    }
+        updateChannelState(CHANNEL_MAIN_POWER);
+        updateChannelState(CHANNEL_MAIN_SOURCE);
+        updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
+        updateChannelState(CHANNEL_MAIN_DSP);
+        updateChannelState(CHANNEL_MAIN_VOLUME);
+        updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
+        updateChannelState(CHANNEL_MAIN_MUTE);
+        updateChannelState(CHANNEL_MAIN_BASS);
+        updateChannelState(CHANNEL_MAIN_TREBLE);
 
-    /**
-     * Handle the received information that zone 3 power is OFF
-     */
-    private void handlePowerOffZone3() {
-        cancelPowerOnZone3Job();
-        powerZone3 = false;
-        updateChannelState(CHANNEL_ZONE3_POWER);
-        updateChannelState(CHANNEL_ZONE3_SOURCE);
-        updateChannelState(CHANNEL_ZONE3_VOLUME);
-        updateChannelState(CHANNEL_ZONE3_MUTE);
+        updateChannelState(CHANNEL_ALL_POWER);
+        updateChannelState(CHANNEL_ALL_BRIGHTNESS);
     }
 
     /**
-     * Handle the received information that zone 4 power is ON
+     * Handle the received information that a zone power is ON
      */
-    private void handlePowerOnZone4() {
-        boolean prev = powerZone4;
-        powerZone4 = true;
-        updateChannelState(CHANNEL_ZONE4_POWER);
-        if (!prev) {
-            schedulePowerOnZone4Job();
+    private void handlePowerOnZone(int numZone) {
+        Boolean prev = powers[numZone];
+        powers[numZone] = true;
+        updateGroupChannelState(numZone, CHANNEL_POWER);
+        if ((prev == null) || !prev) {
+            schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
         }
     }
 
     /**
-     * Handle the received information that zone 4 power is OFF
+     * Handle the received information that a zone power is OFF
      */
-    private void handlePowerOffZone4() {
-        cancelPowerOnZone4Job();
-        powerZone4 = false;
-        updateChannelState(CHANNEL_ZONE4_POWER);
-        updateChannelState(CHANNEL_ZONE4_SOURCE);
-        updateChannelState(CHANNEL_ZONE4_VOLUME);
-        updateChannelState(CHANNEL_ZONE4_MUTE);
+    private void handlePowerOffZone(int numZone) {
+        cancelPowerOnZoneJob(numZone);
+        powers[numZone] = false;
+        updateGroupChannelState(numZone, CHANNEL_POWER);
+        updateGroupChannelState(numZone, CHANNEL_SOURCE);
+        updateGroupChannelState(numZone, CHANNEL_VOLUME);
+        updateGroupChannelState(numZone, CHANNEL_MUTE);
+        updateGroupChannelState(numZone, CHANNEL_BASS);
+        updateGroupChannelState(numZone, CHANNEL_TREBLE);
+        updateGroupChannelState(numZone, CHANNEL_BALANCE);
+        updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
+        updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
     }
 
     /**
@@ -1762,9 +1658,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             logger.debug("Power OFF job");
             handlePowerOff();
             if (switchOffAllZones) {
-                handlePowerOffZone2();
-                handlePowerOffZone3();
-                handlePowerOffZone4();
+                for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                    handlePowerOffZone(zone);
+                }
             }
         }, 2000, TimeUnit.MILLISECONDS);
     }
@@ -1785,20 +1681,20 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
      */
     private void schedulePowerOnJob() {
         logger.debug("Schedule power ON job");
-        cancelPowerOnJob();
-        powerOnJob = scheduler.schedule(() -> {
+        cancelPowerOnZoneJob(0);
+        powerOnZoneJobs[0] = scheduler.schedule(() -> {
             synchronized (sequenceLock) {
                 logger.debug("Power ON job");
                 try {
                     switch (protocol) {
                         case HEX:
                             if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
-                                sendCommand(getVolumeDownCommand());
+                                sendCommand(getVolumeDownCommand(0));
                                 Thread.sleep(100);
-                                sendCommand(getVolumeUpCommand());
+                                sendCommand(getVolumeUpCommand(0));
                                 Thread.sleep(100);
                             }
-                            if (model.getNbAdditionalZones() >= 1) {
+                            if (model.getNumberOfZones() > 1) {
                                 if (currentZone != 1
                                         && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
                                     selectZone(1, model.getZoneSelectCmd());
@@ -1864,8 +1760,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                                     Thread.sleep(SLEEP_INTV);
                                     sendCommand(RotelCommand.TREBLE);
                                     Thread.sleep(SLEEP_INTV);
-                                    sendCommand(RotelCommand.TONE_CONTROLS);
-                                    Thread.sleep(SLEEP_INTV);
+                                    if (model.canGetBypassStatus()) {
+                                        sendCommand(RotelCommand.TONE_CONTROLS);
+                                        Thread.sleep(SLEEP_INTV);
+                                    }
                                 }
                             }
                             if (model.hasBalanceControl()) {
@@ -1873,8 +1771,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                                 Thread.sleep(SLEEP_INTV);
                             }
                             if (model.hasPlayControl()) {
+                                RotelSource source = sources[0];
                                 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
-                                        && (model != RotelModel.RCX1500 || !source.getName().equals("CD"))) {
+                                        && (model != RotelModel.RCX1500 || source == null
+                                                || !source.getName().equals("CD"))) {
                                     sendCommand(RotelCommand.PLAY_STATUS);
                                     Thread.sleep(SLEEP_INTV);
                                 } else {
@@ -1903,7 +1803,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             sendCommand(RotelCommand.UPDATE_AUTO);
                             Thread.sleep(SLEEP_INTV);
                             if (model.hasSourceControl()) {
-                                sendCommand(RotelCommand.SOURCE);
+                                if (model.getNumberOfZones() > 1) {
+                                    sendCommand(RotelCommand.INPUT);
+                                } else {
+                                    sendCommand(RotelCommand.SOURCE);
+                                }
                                 Thread.sleep(SLEEP_INTV);
                             }
                             if (model.hasVolumeControl()) {
@@ -1917,8 +1821,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                                 Thread.sleep(SLEEP_INTV);
                                 sendCommand(RotelCommand.TREBLE);
                                 Thread.sleep(SLEEP_INTV);
-                                sendCommand(RotelCommand.TCBYPASS);
-                                Thread.sleep(SLEEP_INTV);
+                                if (model.canGetBypassStatus()) {
+                                    sendCommand(RotelCommand.TCBYPASS);
+                                    Thread.sleep(SLEEP_INTV);
+                                }
                             }
                             if (model.hasBalanceControl()) {
                                 sendCommand(RotelCommand.BALANCE);
@@ -1927,7 +1833,8 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                             if (model.hasPlayControl()) {
                                 sendCommand(RotelCommand.PLAY_STATUS);
                                 Thread.sleep(SLEEP_INTV);
-                                if (source.getName().equals("CD") && !model.hasSourceControl()) {
+                                RotelSource source = sources[0];
+                                if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
                                     sendCommand(RotelCommand.TRACK);
                                     Thread.sleep(SLEEP_INTV);
                                 }
@@ -1948,6 +1855,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                                 sendCommand(RotelCommand.SPEAKER);
                                 Thread.sleep(SLEEP_INTV);
                             }
+                            sendCommand(RotelCommand.MODEL);
+                            Thread.sleep(SLEEP_INTV);
+                            sendCommand(RotelCommand.VERSION);
+                            Thread.sleep(SLEEP_INTV);
                             break;
                     }
                 } catch (RotelException e) {
@@ -1964,125 +1875,29 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     }
 
     /**
-     * Cancel the job scheduled when the device power (main zone) switched ON
-     */
-    private void cancelPowerOnJob() {
-        ScheduledFuture<?> powerOnJob = this.powerOnJob;
-        if (powerOnJob != null && !powerOnJob.isCancelled()) {
-            powerOnJob.cancel(true);
-            this.powerOnJob = null;
-        }
-    }
-
-    /**
-     * Schedule the job to run with a few seconds delay when the zone 2 power switched ON
-     */
-    private void schedulePowerOnZone2Job() {
-        logger.debug("Schedule power ON zone 2 job");
-        cancelPowerOnZone2Job();
-        powerOnZone2Job = scheduler.schedule(() -> {
-            synchronized (sequenceLock) {
-                logger.debug("Power ON zone 2 job");
-                try {
-                    if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1) {
-                        selectZone(2, model.getZoneSelectCmd());
-                        sendCommand(
-                                model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
-                        Thread.sleep(100);
-                        sendCommand(model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP : RotelCommand.VOLUME_UP);
-                        Thread.sleep(100);
-                    }
-                } catch (RotelException e) {
-                    logger.debug("Init sequence zone 2 failed: {}", e.getMessage());
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                            "@text/offline.comm-error-init-sequence-zone [\"2\"]");
-                    closeConnection();
-                } catch (InterruptedException e) {
-                    logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage());
-                    Thread.currentThread().interrupt();
-                }
-            }
-        }, 2500, TimeUnit.MILLISECONDS);
-    }
-
-    /**
-     * Cancel the job scheduled when the zone 2 power switched ON
-     */
-    private void cancelPowerOnZone2Job() {
-        ScheduledFuture<?> powerOnZone2Job = this.powerOnZone2Job;
-        if (powerOnZone2Job != null && !powerOnZone2Job.isCancelled()) {
-            powerOnZone2Job.cancel(true);
-            this.powerOnZone2Job = null;
-        }
-    }
-
-    /**
-     * Schedule the job to run with a few seconds delay when the zone 3 power switched ON
-     */
-    private void schedulePowerOnZone3Job() {
-        logger.debug("Schedule power ON zone 3 job");
-        cancelPowerOnZone3Job();
-        powerOnZone3Job = scheduler.schedule(() -> {
-            synchronized (sequenceLock) {
-                logger.debug("Power ON zone 3 job");
-                try {
-                    if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 2) {
-                        selectZone(3, model.getZoneSelectCmd());
-                        sendCommand(
-                                model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
-                        Thread.sleep(100);
-                        sendCommand(model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP : RotelCommand.VOLUME_UP);
-                        Thread.sleep(100);
-                    }
-                } catch (RotelException e) {
-                    logger.debug("Init sequence zone 3 failed: {}", e.getMessage());
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                            "@text/offline.comm-error-init-sequence-zone [\"3\"]");
-                    closeConnection();
-                } catch (InterruptedException e) {
-                    logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage());
-                    Thread.currentThread().interrupt();
-                }
-            }
-        }, 2500, TimeUnit.MILLISECONDS);
-    }
-
-    /**
-     * Cancel the job scheduled when the zone 3 power switched ON
-     */
-    private void cancelPowerOnZone3Job() {
-        ScheduledFuture<?> powerOnZone3Job = this.powerOnZone3Job;
-        if (powerOnZone3Job != null && !powerOnZone3Job.isCancelled()) {
-            powerOnZone3Job.cancel(true);
-            this.powerOnZone3Job = null;
-        }
-    }
-
-    /**
-     * Schedule the job to run with a few seconds delay when the zone 4 power switched ON
+     * Schedule the job to run with a few seconds delay when the zone power switched ON
      */
-    private void schedulePowerOnZone4Job() {
-        logger.debug("Schedule power ON zone 4 job");
-        cancelPowerOnZone4Job();
-        powerOnZone4Job = scheduler.schedule(() -> {
+    private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
+        logger.debug("Schedule power ON zone {} job", numZone);
+        cancelPowerOnZoneJob(numZone);
+        powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
             synchronized (sequenceLock) {
-                logger.debug("Power ON zone 4 job");
+                logger.debug("Power ON zone {} job", numZone);
                 try {
-                    if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 3) {
-                        selectZone(4, model.getZoneSelectCmd());
-                        sendCommand(
-                                model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
+                    if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
+                        selectZone(numZone, model.getZoneSelectCmd());
+                        sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
                         Thread.sleep(100);
-                        sendCommand(model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP : RotelCommand.VOLUME_UP);
+                        sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
                         Thread.sleep(100);
                     }
                 } catch (RotelException e) {
-                    logger.debug("Init sequence zone 4 failed: {}", e.getMessage());
+                    logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                            "@text/offline.comm-error-init-sequence-zone [\"4\"]");
+                            String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
                     closeConnection();
                 } catch (InterruptedException e) {
-                    logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage());
+                    logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
                     Thread.currentThread().interrupt();
                 }
             }
@@ -2090,13 +1905,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     }
 
     /**
-     * Cancel the job scheduled when the zone 4 power switched ON
+     * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
      */
-    private void cancelPowerOnZone4Job() {
-        ScheduledFuture<?> powerOnZone4Job = this.powerOnZone4Job;
-        if (powerOnZone4Job != null && !powerOnZone4Job.isCancelled()) {
-            powerOnZone4Job.cancel(true);
-            this.powerOnZone4Job = null;
+    private void cancelPowerOnZoneJob(int numZone) {
+        ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
+        if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
+            powerOnZoneJob.cancel(true);
+            powerOnZoneJobs[numZone] = null;
         }
     }
 
@@ -2110,7 +1925,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             if (!connector.isConnected()) {
                 logger.debug("Trying to reconnect...");
                 closeConnection();
-                power = null;
+                powers[0] = null;
                 String error = null;
                 if (openConnection()) {
                     synchronized (sequenceLock) {
@@ -2129,9 +1944,9 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 }
                 if (error != null) {
                     handlePowerOff();
-                    handlePowerOffZone2();
-                    handlePowerOffZone3();
-                    handlePowerOffZone4();
+                    for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
+                        handlePowerOffZone(zone);
+                    }
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
                 } else {
                     updateStatus(ThingStatus.ONLINE);
@@ -2151,6 +1966,10 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
         }
     }
 
+    private void updateGroupChannelState(int numZone, String channel) {
+        updateChannelState(String.format("zone%d#%s", numZone, channel));
+    }
+
     /**
      * Update the state of a channel
      *
@@ -2161,51 +1980,79 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
             return;
         }
         State state = UnDefType.UNDEF;
+        RotelSource localSource;
+        int numZone = 0;
         switch (channel) {
-            case CHANNEL_POWER:
-            case CHANNEL_MAIN_POWER:
-                Boolean po = power;
-                if (po != null) {
-                    state = OnOffType.from(po.booleanValue());
-                }
+            case CHANNEL_ZONE1_SOURCE:
+            case CHANNEL_ZONE1_VOLUME:
+            case CHANNEL_ZONE1_MUTE:
+            case CHANNEL_ZONE1_BASS:
+            case CHANNEL_ZONE1_TREBLE:
+            case CHANNEL_ZONE1_BALANCE:
+            case CHANNEL_ZONE1_FREQUENCY:
+                numZone = 1;
                 break;
             case CHANNEL_ZONE2_POWER:
-                state = OnOffType.from(powerZone2);
+            case CHANNEL_ZONE2_SOURCE:
+            case CHANNEL_ZONE2_VOLUME:
+            case CHANNEL_ZONE2_VOLUME_UP_DOWN:
+            case CHANNEL_ZONE2_MUTE:
+            case CHANNEL_ZONE2_BASS:
+            case CHANNEL_ZONE2_TREBLE:
+            case CHANNEL_ZONE2_BALANCE:
+            case CHANNEL_ZONE2_FREQUENCY:
+                numZone = 2;
                 break;
             case CHANNEL_ZONE3_POWER:
-                state = OnOffType.from(powerZone3);
+            case CHANNEL_ZONE3_SOURCE:
+            case CHANNEL_ZONE3_VOLUME:
+            case CHANNEL_ZONE3_MUTE:
+            case CHANNEL_ZONE3_BASS:
+            case CHANNEL_ZONE3_TREBLE:
+            case CHANNEL_ZONE3_BALANCE:
+            case CHANNEL_ZONE3_FREQUENCY:
+                numZone = 3;
                 break;
             case CHANNEL_ZONE4_POWER:
-                state = OnOffType.from(powerZone4);
+            case CHANNEL_ZONE4_SOURCE:
+            case CHANNEL_ZONE4_VOLUME:
+            case CHANNEL_ZONE4_MUTE:
+            case CHANNEL_ZONE4_BASS:
+            case CHANNEL_ZONE4_TREBLE:
+            case CHANNEL_ZONE4_BALANCE:
+            case CHANNEL_ZONE4_FREQUENCY:
+                numZone = 4;
                 break;
-            case CHANNEL_SOURCE:
-            case CHANNEL_MAIN_SOURCE:
-                if (isPowerOn()) {
-                    state = new StringType(source.getName());
-                }
+            default:
                 break;
-            case CHANNEL_MAIN_RECORD_SOURCE:
-                RotelSource recordSource = this.recordSource;
-                if (isPowerOn() && recordSource != null) {
-                    state = new StringType(recordSource.getName());
+        }
+        switch (channel) {
+            case CHANNEL_POWER:
+            case CHANNEL_MAIN_POWER:
+            case CHANNEL_ALL_POWER:
+            case CHANNEL_ZONE2_POWER:
+            case CHANNEL_ZONE3_POWER:
+            case CHANNEL_ZONE4_POWER:
+                Boolean powerZone = powers[numZone];
+                if (powerZone != null) {
+                    state = OnOffType.from(powerZone.booleanValue());
                 }
                 break;
+            case CHANNEL_SOURCE:
+            case CHANNEL_MAIN_SOURCE:
+            case CHANNEL_ZONE1_SOURCE:
             case CHANNEL_ZONE2_SOURCE:
-                RotelSource sourceZone2 = this.sourceZone2;
-                if (powerZone2 && sourceZone2 != null) {
-                    state = new StringType(sourceZone2.getName());
-                }
-                break;
             case CHANNEL_ZONE3_SOURCE:
-                RotelSource sourceZone3 = this.sourceZone3;
-                if (powerZone3 && sourceZone3 != null) {
-                    state = new StringType(sourceZone3.getName());
+            case CHANNEL_ZONE4_SOURCE:
+                localSource = sources[numZone];
+                if (isPowerOn(numZone) && localSource != null) {
+                    state = new StringType(localSource.getName());
                 }
                 break;
-            case CHANNEL_ZONE4_SOURCE:
-                RotelSource sourceZone4 = this.sourceZone4;
-                if (powerZone4 && sourceZone4 != null) {
-                    state = new StringType(sourceZone4.getName());
+            case CHANNEL_MAIN_RECORD_SOURCE:
+                localSource = recordSource;
+                if (isPowerOn() && localSource != null) {
+                    state = new StringType(localSource.getName());
                 }
                 break;
             case CHANNEL_DSP:
@@ -2216,78 +2063,54 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 break;
             case CHANNEL_VOLUME:
             case CHANNEL_MAIN_VOLUME:
-                if (isPowerOn()) {
-                    long volumePct = Math
-                            .round((double) (volume - minVolume) / (double) (maxVolume - minVolume) * 100.0);
-                    state = new PercentType(BigDecimal.valueOf(volumePct));
-                }
-                break;
-            case CHANNEL_MAIN_VOLUME_UP_DOWN:
-                if (isPowerOn()) {
-                    state = new DecimalType(volume);
-                }
-                break;
+            case CHANNEL_ZONE1_VOLUME:
             case CHANNEL_ZONE2_VOLUME:
-                if (powerZone2 && !fixedVolumeZone2) {
-                    long volumePct = Math
-                            .round((double) (volumeZone2 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
-                    state = new PercentType(BigDecimal.valueOf(volumePct));
-                }
-                break;
-            case CHANNEL_ZONE2_VOLUME_UP_DOWN:
-                if (powerZone2 && !fixedVolumeZone2) {
-                    state = new DecimalType(volumeZone2);
-                }
-                break;
             case CHANNEL_ZONE3_VOLUME:
-                if (powerZone3 && !fixedVolumeZone3) {
+            case CHANNEL_ZONE4_VOLUME:
+                if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
                     long volumePct = Math
-                            .round((double) (volumeZone3 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
+                            .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
                     state = new PercentType(BigDecimal.valueOf(volumePct));
                 }
                 break;
-            case CHANNEL_ZONE4_VOLUME:
-                if (powerZone4 && !fixedVolumeZone4) {
-                    long volumePct = Math
-                            .round((double) (volumeZone4 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
-                    state = new PercentType(BigDecimal.valueOf(volumePct));
+            case CHANNEL_MAIN_VOLUME_UP_DOWN:
+            case CHANNEL_ZONE2_VOLUME_UP_DOWN:
+                if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
+                    state = new DecimalType(volumes[numZone]);
                 }
                 break;
             case CHANNEL_MUTE:
             case CHANNEL_MAIN_MUTE:
-                if (isPowerOn()) {
-                    state = OnOffType.from(mute);
-                }
-                break;
+            case CHANNEL_ZONE1_MUTE:
             case CHANNEL_ZONE2_MUTE:
-                if (powerZone2) {
-                    state = OnOffType.from(muteZone2);
-                }
-                break;
             case CHANNEL_ZONE3_MUTE:
-                if (powerZone3) {
-                    state = OnOffType.from(muteZone3);
-                }
-                break;
             case CHANNEL_ZONE4_MUTE:
-                if (powerZone4) {
-                    state = OnOffType.from(muteZone4);
+                if (isPowerOn(numZone)) {
+                    state = OnOffType.from(mutes[numZone]);
                 }
                 break;
             case CHANNEL_BASS:
             case CHANNEL_MAIN_BASS:
-                if (isPowerOn()) {
-                    state = new DecimalType(bass);
+            case CHANNEL_ZONE1_BASS:
+            case CHANNEL_ZONE2_BASS:
+            case CHANNEL_ZONE3_BASS:
+            case CHANNEL_ZONE4_BASS:
+                if (isPowerOn(numZone)) {
+                    state = new DecimalType(basses[numZone]);
                 }
                 break;
             case CHANNEL_TREBLE:
             case CHANNEL_MAIN_TREBLE:
-                if (isPowerOn()) {
-                    state = new DecimalType(treble);
+            case CHANNEL_ZONE1_TREBLE:
+            case CHANNEL_ZONE2_TREBLE:
+            case CHANNEL_ZONE3_TREBLE:
+            case CHANNEL_ZONE4_TREBLE:
+                if (isPowerOn(numZone)) {
+                    state = new DecimalType(trebles[numZone]);
                 }
                 break;
             case CHANNEL_TRACK:
-                if (track > 0 && isPowerOn()) {
+                if (isPowerOn() && track > 0) {
                     state = new DecimalType(track);
                 }
                 break;
@@ -2305,8 +2128,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 }
                 break;
             case CHANNEL_FREQUENCY:
-                if (frequency > 0.0 && isPowerOn()) {
-                    state = new DecimalType(frequency);
+            case CHANNEL_ZONE1_FREQUENCY:
+            case CHANNEL_ZONE2_FREQUENCY:
+            case CHANNEL_ZONE3_FREQUENCY:
+            case CHANNEL_ZONE4_FREQUENCY:
+                if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
+                    state = new DecimalType(frequencies[numZone]);
                 }
                 break;
             case CHANNEL_LINE1:
@@ -2316,6 +2143,7 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 state = new StringType(frontPanelLine2);
                 break;
             case CHANNEL_BRIGHTNESS:
+            case CHANNEL_ALL_BRIGHTNESS:
                 if (isPowerOn() && model.hasDimmerControl()) {
                     long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
                             / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
@@ -2328,8 +2156,12 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
                 }
                 break;
             case CHANNEL_BALANCE:
-                if (isPowerOn()) {
-                    state = new DecimalType(balance);
+            case CHANNEL_ZONE1_BALANCE:
+            case CHANNEL_ZONE2_BALANCE:
+            case CHANNEL_ZONE3_BALANCE:
+            case CHANNEL_ZONE4_BALANCE:
+                if (isPowerOn(numZone)) {
+                    state = new DecimalType(balances[numZone]);
                 }
                 break;
             case CHANNEL_SPEAKER_A:
@@ -2349,76 +2181,433 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL
     }
 
     /**
-     * Inform about the main zone power state
+     * Inform about the device / main zone power state
      *
-     * @return true if main zone power state is known and known as ON
+     * @return true if device / main zone power state is known and known as ON
      */
     private boolean isPowerOn() {
-        Boolean power = this.power;
-        return power != null && power.booleanValue();
+        return isPowerOn(0);
+    }
+
+    /**
+     * Inform about the power state
+     *
+     * @param numZone the zone number (1-4) or 0 for the device or main zone
+     *
+     * @return true if power state is known and known as ON
+     */
+    private boolean isPowerOn(int numZone) {
+        if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
+            throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
+        }
+        Boolean power = powers[numZone];
+        return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
     }
 
     /**
-     * Get the command to be used for main zone POWER ON
+     * Get the command to be used for POWER ON
+     *
+     * @param numZone the zone number (2-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getPowerOnCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
+    private RotelCommand getPowerOnCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
+            case 2:
+                return RotelCommand.ZONE2_POWER_ON;
+            case 3:
+                return RotelCommand.ZONE3_POWER_ON;
+            case 4:
+                return RotelCommand.ZONE4_POWER_ON;
+            default:
+                throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
+        }
     }
 
     /**
-     * Get the command to be used for main zone POWER OFF
+     * Get the command to be used for POWER OFF
+     *
+     * @param numZone the zone number (2-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getPowerOffCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
+    private RotelCommand getPowerOffCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
+            case 2:
+                return RotelCommand.ZONE2_POWER_OFF;
+            case 3:
+                return RotelCommand.ZONE3_POWER_OFF;
+            case 4:
+                return RotelCommand.ZONE4_POWER_OFF;
+            default:
+                throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
+        }
     }
 
     /**
-     * Get the command to be used for main zone VOLUME UP
+     * Get the command to be used for VOLUME UP
+     *
+     * @param numZone the zone number (1-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getVolumeUpCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
+    private RotelCommand getVolumeUpCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
+            case 1:
+                return RotelCommand.ZONE1_VOLUME_UP;
+            case 2:
+                return RotelCommand.ZONE2_VOLUME_UP;
+            case 3:
+                return RotelCommand.ZONE3_VOLUME_UP;
+            case 4:
+                return RotelCommand.ZONE4_VOLUME_UP;
+            default:
+                throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
+        }
     }
 
     /**
-     * Get the command to be used for main zone VOLUME DOWN
+     * Get the command to be used for VOLUME DOWN
+     *
+     * @param numZone the zone number (1-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getVolumeDownCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN : RotelCommand.VOLUME_DOWN;
+    private RotelCommand getVolumeDownCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
+                        : RotelCommand.VOLUME_DOWN;
+            case 1:
+                return RotelCommand.ZONE1_VOLUME_DOWN;
+            case 2:
+                return RotelCommand.ZONE2_VOLUME_DOWN;
+            case 3:
+                return RotelCommand.ZONE3_VOLUME_DOWN;
+            case 4:
+                return RotelCommand.ZONE4_VOLUME_DOWN;
+            default:
+                throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for VOLUME SET
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getVolumeSetCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.VOLUME_SET;
+            case 1:
+                return RotelCommand.ZONE1_VOLUME_SET;
+            case 2:
+                return RotelCommand.ZONE2_VOLUME_SET;
+            case 3:
+                return RotelCommand.ZONE3_VOLUME_SET;
+            case 4:
+                return RotelCommand.ZONE4_VOLUME_SET;
+            default:
+                throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
+        }
     }
 
     /**
-     * Get the command to be used for main zone MUTE ON
+     * Get the command to be used for MUTE ON
+     *
+     * @param numZone the zone number (1-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getMuteOnCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
+    private RotelCommand getMuteOnCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
+            case 1:
+                return RotelCommand.ZONE1_MUTE_ON;
+            case 2:
+                return RotelCommand.ZONE2_MUTE_ON;
+            case 3:
+                return RotelCommand.ZONE3_MUTE_ON;
+            case 4:
+                return RotelCommand.ZONE4_MUTE_ON;
+            default:
+                throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
+        }
     }
 
     /**
-     * Get the command to be used for main zone MUTE OFF
+     * Get the command to be used for MUTE OFF
+     *
+     * @param numZone the zone number (1-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getMuteOffCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
+    private RotelCommand getMuteOffCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
+            case 1:
+                return RotelCommand.ZONE1_MUTE_OFF;
+            case 2:
+                return RotelCommand.ZONE2_MUTE_OFF;
+            case 3:
+                return RotelCommand.ZONE3_MUTE_OFF;
+            case 4:
+                return RotelCommand.ZONE4_MUTE_OFF;
+            default:
+                throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
+        }
     }
 
     /**
-     * Get the command to be used for main zone MUTE TOGGLE
+     * Get the command to be used for MUTE TOGGLE
+     *
+     * @param numZone the zone number (1-4) or 0 for the device or main zone
      *
      * @return the command
      */
-    private RotelCommand getMuteToggleCommand() {
-        return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE : RotelCommand.MUTE_TOGGLE;
+    private RotelCommand getMuteToggleCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
+                        : RotelCommand.MUTE_TOGGLE;
+            case 1:
+                return RotelCommand.ZONE1_MUTE_TOGGLE;
+            case 2:
+                return RotelCommand.ZONE2_MUTE_TOGGLE;
+            case 3:
+                return RotelCommand.ZONE3_MUTE_TOGGLE;
+            case 4:
+                return RotelCommand.ZONE4_MUTE_TOGGLE;
+            default:
+                throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for BASS UP
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getBassUpCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.BASS_UP;
+            case 1:
+                return RotelCommand.ZONE1_BASS_UP;
+            case 2:
+                return RotelCommand.ZONE2_BASS_UP;
+            case 3:
+                return RotelCommand.ZONE3_BASS_UP;
+            case 4:
+                return RotelCommand.ZONE4_BASS_UP;
+            default:
+                throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for BASS DOWN
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getBassDownCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.BASS_DOWN;
+            case 1:
+                return RotelCommand.ZONE1_BASS_DOWN;
+            case 2:
+                return RotelCommand.ZONE2_BASS_DOWN;
+            case 3:
+                return RotelCommand.ZONE3_BASS_DOWN;
+            case 4:
+                return RotelCommand.ZONE4_BASS_DOWN;
+            default:
+                throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for BASS SET
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getBassSetCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.BASS_SET;
+            case 1:
+                return RotelCommand.ZONE1_BASS_SET;
+            case 2:
+                return RotelCommand.ZONE2_BASS_SET;
+            case 3:
+                return RotelCommand.ZONE3_BASS_SET;
+            case 4:
+                return RotelCommand.ZONE4_BASS_SET;
+            default:
+                throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for TREBLE UP
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getTrebleUpCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.TREBLE_UP;
+            case 1:
+                return RotelCommand.ZONE1_TREBLE_UP;
+            case 2:
+                return RotelCommand.ZONE2_TREBLE_UP;
+            case 3:
+                return RotelCommand.ZONE3_TREBLE_UP;
+            case 4:
+                return RotelCommand.ZONE4_TREBLE_UP;
+            default:
+                throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for TREBLE DOWN
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getTrebleDownCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.TREBLE_DOWN;
+            case 1:
+                return RotelCommand.ZONE1_TREBLE_DOWN;
+            case 2:
+                return RotelCommand.ZONE2_TREBLE_DOWN;
+            case 3:
+                return RotelCommand.ZONE3_TREBLE_DOWN;
+            case 4:
+                return RotelCommand.ZONE4_TREBLE_DOWN;
+            default:
+                throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for TREBLE SET
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getTrebleSetCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.TREBLE_SET;
+            case 1:
+                return RotelCommand.ZONE1_TREBLE_SET;
+            case 2:
+                return RotelCommand.ZONE2_TREBLE_SET;
+            case 3:
+                return RotelCommand.ZONE3_TREBLE_SET;
+            case 4:
+                return RotelCommand.ZONE4_TREBLE_SET;
+            default:
+                throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for BALANCE LEFT
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getBalanceLeftCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.BALANCE_LEFT;
+            case 1:
+                return RotelCommand.ZONE1_BALANCE_LEFT;
+            case 2:
+                return RotelCommand.ZONE2_BALANCE_LEFT;
+            case 3:
+                return RotelCommand.ZONE3_BALANCE_LEFT;
+            case 4:
+                return RotelCommand.ZONE4_BALANCE_LEFT;
+            default:
+                throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for BALANCE RIGHT
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getBalanceRightCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.BALANCE_RIGHT;
+            case 1:
+                return RotelCommand.ZONE1_BALANCE_RIGHT;
+            case 2:
+                return RotelCommand.ZONE2_BALANCE_RIGHT;
+            case 3:
+                return RotelCommand.ZONE3_BALANCE_RIGHT;
+            case 4:
+                return RotelCommand.ZONE4_BALANCE_RIGHT;
+            default:
+                throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
+        }
+    }
+
+    /**
+     * Get the command to be used for BALANCE SET
+     *
+     * @param numZone the zone number (1-4) or 0 for the device
+     *
+     * @return the command
+     */
+    private RotelCommand getBalanceSetCommand(int numZone) {
+        switch (numZone) {
+            case 0:
+                return RotelCommand.BALANCE_SET;
+            case 1:
+                return RotelCommand.ZONE1_BALANCE_SET;
+            case 2:
+                return RotelCommand.ZONE2_BALANCE_SET;
+            case 3:
+                return RotelCommand.ZONE3_BALANCE_SET;
+            case 4:
+                return RotelCommand.ZONE4_BALANCE_SET;
+            default:
+                throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
+        }
     }
 
     private void sendCommand(RotelCommand cmd) throws RotelException {
index c90f5a8cdc1f4f915c08e458c2cd7dcbe4a81681..ab406c8cda11eb27349637885dc4725fad77542c 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.rotel.internal.protocol.ascii;
 
+import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
+
 import java.nio.charset.StandardCharsets;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -58,10 +60,22 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl
         if (value != null) {
             switch (cmd) {
                 case VOLUME_SET:
+                case ZONE1_VOLUME_SET:
+                case ZONE2_VOLUME_SET:
+                case ZONE3_VOLUME_SET:
+                case ZONE4_VOLUME_SET:
                     messageStr += String.format("%02d", value);
                     break;
                 case BASS_SET:
+                case ZONE1_BASS_SET:
+                case ZONE2_BASS_SET:
+                case ZONE3_BASS_SET:
+                case ZONE4_BASS_SET:
                 case TREBLE_SET:
+                case ZONE1_TREBLE_SET:
+                case ZONE2_TREBLE_SET:
+                case ZONE3_TREBLE_SET:
+                case ZONE4_TREBLE_SET:
                     if (value == 0) {
                         messageStr += "000";
                     } else if (value > 0) {
@@ -71,6 +85,10 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl
                     }
                     break;
                 case BALANCE_SET:
+                case ZONE1_BALANCE_SET:
+                case ZONE2_BALANCE_SET:
+                case ZONE3_BALANCE_SET:
+                case ZONE4_BALANCE_SET:
                     if (value == 0) {
                         messageStr += "000";
                     } else if (value > 0) {
@@ -97,4 +115,36 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl
         logger.debug("Command \"{}\" => {}", cmd.getName(), messageStr);
         return message;
     }
+
+    @Override
+    protected void dispatchKeyValue(String key, String value) {
+        // For distribution amplifiers, we need to split certain values to get the value for each zone
+        if (model == RotelModel.C8 && value.contains(",")) {
+            switch (key) {
+                case KEY_INPUT:
+                case KEY_VOLUME:
+                case KEY_MUTE:
+                case KEY_BASS:
+                case KEY_TREBLE:
+                case KEY_BALANCE:
+                case KEY_FREQ:
+                    String[] splitValues = value.split(",");
+                    int nb = splitValues.length;
+                    if (nb > MAX_NUMBER_OF_ZONES) {
+                        nb = MAX_NUMBER_OF_ZONES;
+                    }
+                    for (int i = 1; i <= nb; i++) {
+                        String val = KEY_INPUT.equals(key) ? String.format("z%d:input_%s", i, splitValues[i - 1])
+                                : splitValues[i - 1];
+                        dispatchKeyValue(String.format("%s_zone%d", key, i), val);
+                    }
+                    break;
+                default:
+                    super.dispatchKeyValue(key, value);
+                    break;
+            }
+        } else {
+            super.dispatchKeyValue(key, value);
+        }
+    }
 }
index d372033478797a9367b2a3769e4e568487129e49..0214b96e6a8418f093ecd8dd8dd81df8b17ddaff 100644 (file)
@@ -675,7 +675,7 @@ public class RotelHexProtocolHandler extends RotelAbstractProtocolHandler {
         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
             RotelSource source = parseSource(value, true);
             if (source != null) {
-                RotelCommand cmd = source.getZone2Command();
+                RotelCommand cmd = source.getZoneCommand(2);
                 if (cmd != null) {
                     value = cmd.getAsciiCommandV2();
                     if (value != null) {
@@ -709,7 +709,7 @@ public class RotelHexProtocolHandler extends RotelAbstractProtocolHandler {
         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
             RotelSource source = parseSource(value, true);
             if (source != null) {
-                RotelCommand cmd = source.getZone3Command();
+                RotelCommand cmd = source.getZoneCommand(3);
                 if (cmd != null) {
                     value = cmd.getAsciiCommandV2();
                     if (value != null) {
@@ -743,7 +743,7 @@ public class RotelHexProtocolHandler extends RotelAbstractProtocolHandler {
         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
             RotelSource source = parseSource(value, true);
             if (source != null) {
-                RotelCommand cmd = source.getZone4Command();
+                RotelCommand cmd = source.getZoneCommand(4);
                 if (cmd != null) {
                     value = cmd.getAsciiCommandV2();
                     if (value != null) {
index 6308b2ce9df4fb86ef60a93fb4eff85c6cb4e605..80055551670656dc225c401600264caf0caa1a07 100644 (file)
@@ -170,8 +170,14 @@ config.serialPort.description = Serial port to use for connecting to the Rotel d
 
 # channel group types
 
+channel-group.allZones.label = All Zones
+channel-group.allZones.description = The controls applied to all zones
 channel-group.mainZone.label = Main Zone
 channel-group.mainZone.description = The controls of the main zone
+channel-group.zone.label = Zone
+channel-group.zone.description = The controls of the zone
+channel-group.zone1.label = Zone 1
+channel-group.zone1.description = The controls of the zone 1
 channel-group.zone2.label = Zone 2
 channel-group.zone2.description = The controls of the zone 2
 channel-group.zone3.label = Zone 3
@@ -261,3 +267,7 @@ source.DAB = DAB
 source.PLAYFI = PlayFi
 source.IRADIO = iRadio
 source.NETWORK = Network
+source.INPUTA = Input A
+source.INPUTB = Input B
+source.INPUTC = Input C
+source.INPUTD = Input D
diff --git a/bundles/org.openhab.binding.rotel/src/main/resources/OH-INF/thing/c8.xml b/bundles/org.openhab.binding.rotel/src/main/resources/OH-INF/thing/c8.xml
new file mode 100644 (file)
index 0000000..dc27700
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="rotel"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Rotel C8 Connection Thing Type -->
+       <thing-type id="c8">
+               <label>C8 Distribution Amplifier</label>
+               <description>Connection to the Rotel C8 or C8+ distribution amplifier</description>
+
+               <channel-groups>
+                       <channel-group id="allZones" typeId="allZones"/>
+                       <channel-group id="zone1" typeId="zone">
+                               <label>@text/channel-group.zone1.label</label>
+                               <description>@text/channel-group.zone1.description</description>
+                       </channel-group>
+                       <channel-group id="zone2" typeId="zone">
+                               <label>@text/channel-group.zone2.label</label>
+                               <description>@text/channel-group.zone2.description</description>
+                       </channel-group>
+                       <channel-group id="zone3" typeId="zone">
+                               <label>@text/channel-group.zone3.label</label>
+                               <description>@text/channel-group.zone3.description</description>
+                       </channel-group>
+                       <channel-group id="zone4" typeId="zone">
+                               <label>@text/channel-group.zone4.label</label>
+                               <description>@text/channel-group.zone4.description</description>
+                       </channel-group>
+               </channel-groups>
+
+               <properties>
+                       <property name="protocol">ASCII_V2</property>
+               </properties>
+
+               <config-description-ref uri="thing-type:rotel:serialandip2"/>
+       </thing-type>
+
+</thing:thing-descriptions>
index 261e3d9f68c4fced97f37f076815fd7a8064ac16..8cdfe8a9ee491641a6088f87ad0d3d716cd36d0b 100644 (file)
                </channels>
        </channel-group-type>
 
+       <channel-group-type id="allZones">
+               <label>@text/channel-group.allZones.label</label>
+               <description>@text/channel-group.allZones.description</description>
+               <channels>
+                       <channel id="power" typeId="system.power"/>
+                       <channel id="brightness" typeId="brightness"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="zone">
+               <label>@text/channel-group.zone.label</label>
+               <description>@text/channel-group.zone.description</description>
+               <channels>
+                       <channel id="source" typeId="source"/>
+                       <channel id="volume" typeId="system.volume"/>
+                       <channel id="mute" typeId="system.mute"/>
+                       <channel id="bass" typeId="bass"/>
+                       <channel id="treble" typeId="treble"/>
+                       <channel id="balance" typeId="balance"/>
+                       <channel id="frequency" typeId="frequency"/>
+               </channels>
+       </channel-group-type>
+
        <channel-type id="source">
                <item-type>String</item-type>
                <label>Source Input</label>