]> git.basschouten.com Git - openhab-addons.git/blob
dd5ef9f1efae7966ecd5be6dbaf9cef08fde98ba
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.rotel.internal.handler;
14
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.EventObject;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelBindingConstants;
29 import org.openhab.binding.rotel.internal.RotelCommandDescriptionOptionProvider;
30 import org.openhab.binding.rotel.internal.RotelException;
31 import org.openhab.binding.rotel.internal.RotelModel;
32 import org.openhab.binding.rotel.internal.RotelPlayStatus;
33 import org.openhab.binding.rotel.internal.RotelRepeatMode;
34 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
35 import org.openhab.binding.rotel.internal.communication.RotelCommand;
36 import org.openhab.binding.rotel.internal.communication.RotelConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelDsp;
38 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
40 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
41 import org.openhab.binding.rotel.internal.communication.RotelSource;
42 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
43 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
44 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
45 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
46 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
47 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
48 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
49 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
50 import org.openhab.core.io.transport.serial.SerialPortManager;
51 import org.openhab.core.library.types.DecimalType;
52 import org.openhab.core.library.types.IncreaseDecreaseType;
53 import org.openhab.core.library.types.NextPreviousType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.PercentType;
56 import org.openhab.core.library.types.PlayPauseType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.BaseThingHandler;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.CommandOption;
65 import org.openhab.core.types.RefreshType;
66 import org.openhab.core.types.State;
67 import org.openhab.core.types.StateOption;
68 import org.openhab.core.types.UnDefType;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 /**
73  * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
74  *
75  * @author Laurent Garnier - Initial contribution
76  */
77 @NonNullByDefault
78 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
79
80     private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
81
82     private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
83     private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
84     private static final boolean USE_SIMULATED_DEVICE = false;
85     private static final int SLEEP_INTV = 30;
86
87     private final RotelStateDescriptionOptionProvider stateDescriptionProvider;
88     private final RotelCommandDescriptionOptionProvider commandDescriptionProvider;
89     private final SerialPortManager serialPortManager;
90
91     private @Nullable ScheduledFuture<?> reconnectJob;
92     private @Nullable ScheduledFuture<?> powerOffJob;
93     private @Nullable ScheduledFuture<?>[] powerOnZoneJobs = { null, null, null, null, null };
94
95     private RotelModel model;
96     private RotelProtocol protocol;
97     private RotelAbstractProtocolHandler protocolHandler;
98     private RotelConnector connector;
99
100     private int minVolume;
101     private int maxVolume;
102     private int minToneLevel;
103     private int maxToneLevel;
104
105     private int currentZone = 1;
106     private boolean selectingRecord;
107     private @Nullable Boolean[] powers = { null, false, false, false, false };
108     private boolean powerControlPerZone;
109     private @Nullable RotelSource recordSource;
110     private @Nullable RotelSource[] sources = { RotelSource.CAT0_CD, null, null, null, null };
111     private RotelDsp dsp = RotelDsp.CAT1_NONE;
112     private boolean[] fixedVolumeZones = { false, false, false, false, false };
113     private int[] volumes = { 0, 0, 0, 0, 0 };
114     private boolean[] mutes = { false, false, false, false, false };
115     private int[] basses = { 0, 0, 0, 0, 0 };
116     private int[] trebles = { 0, 0, 0, 0, 0 };
117     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
118     private int track;
119     private boolean randomMode;
120     private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
121     private int radioPreset;
122     private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
123     private String frontPanelLine1 = "";
124     private String frontPanelLine2 = "";
125     private int brightness;
126     private boolean tcbypass;
127     private int[] balances = { 0, 0, 0, 0, 0 };
128     private int minBalanceLevel;
129     private int maxBalanceLevel;
130     private boolean speakera;
131     private boolean speakerb;
132
133     private Object sequenceLock = new Object();
134
135     /**
136      * Constructor
137      */
138     public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
139             RotelCommandDescriptionOptionProvider commandDescriptionProvider, SerialPortManager serialPortManager) {
140         super(thing);
141         this.stateDescriptionProvider = stateDescriptionProvider;
142         this.commandDescriptionProvider = commandDescriptionProvider;
143         this.serialPortManager = serialPortManager;
144         this.model = DEFAULT_MODEL;
145         this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
146         this.protocol = protocolHandler.getProtocol();
147         this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
148     }
149
150     @Override
151     public void initialize() {
152         logger.debug("Start initializing handler for thing {}", getThing().getUID());
153
154         RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
155
156         protocol = RotelProtocol.HEX;
157         if (config.protocol != null && !config.protocol.isEmpty()) {
158             try {
159                 protocol = RotelProtocol.getFromName(config.protocol);
160             } catch (RotelException e) {
161                 // Invalid protocol name in configuration, HEX will be considered by default
162             }
163         } else {
164             Map<String, String> properties = editProperties();
165             String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
166             if (property != null && !property.isEmpty()) {
167                 try {
168                     protocol = RotelProtocol.getFromName(property);
169                 } catch (RotelException e) {
170                     // Invalid protocol name in thing property, HEX will be considered by default
171                 }
172             }
173         }
174         logger.debug("rotelProtocol {}", protocol.getName());
175
176         switch (getThing().getThingTypeUID().getId()) {
177             case THING_TYPE_ID_RSP1066:
178                 model = RotelModel.RSP1066;
179                 break;
180             case THING_TYPE_ID_RSP1068:
181                 model = RotelModel.RSP1068;
182                 break;
183             case THING_TYPE_ID_RSP1069:
184                 model = RotelModel.RSP1069;
185                 break;
186             case THING_TYPE_ID_RSP1098:
187                 model = RotelModel.RSP1098;
188                 break;
189             case THING_TYPE_ID_RSP1570:
190                 model = RotelModel.RSP1570;
191                 break;
192             case THING_TYPE_ID_RSP1572:
193                 model = RotelModel.RSP1572;
194                 break;
195             case THING_TYPE_ID_RSX1055:
196                 model = RotelModel.RSX1055;
197                 break;
198             case THING_TYPE_ID_RSX1056:
199                 model = RotelModel.RSX1056;
200                 break;
201             case THING_TYPE_ID_RSX1057:
202                 model = RotelModel.RSX1057;
203                 break;
204             case THING_TYPE_ID_RSX1058:
205                 model = RotelModel.RSX1058;
206                 break;
207             case THING_TYPE_ID_RSX1065:
208                 model = RotelModel.RSX1065;
209                 break;
210             case THING_TYPE_ID_RSX1067:
211                 model = RotelModel.RSX1067;
212                 break;
213             case THING_TYPE_ID_RSX1550:
214                 model = RotelModel.RSX1550;
215                 break;
216             case THING_TYPE_ID_RSX1560:
217                 model = RotelModel.RSX1560;
218                 break;
219             case THING_TYPE_ID_RSX1562:
220                 model = RotelModel.RSX1562;
221                 break;
222             case THING_TYPE_ID_A11:
223                 model = RotelModel.A11;
224                 break;
225             case THING_TYPE_ID_A12:
226                 model = RotelModel.A12;
227                 break;
228             case THING_TYPE_ID_A14:
229                 model = RotelModel.A14;
230                 break;
231             case THING_TYPE_ID_CD11:
232                 model = RotelModel.CD11;
233                 break;
234             case THING_TYPE_ID_CD14:
235                 model = RotelModel.CD14;
236                 break;
237             case THING_TYPE_ID_RA11:
238                 model = RotelModel.RA11;
239                 break;
240             case THING_TYPE_ID_RA12:
241                 model = RotelModel.RA12;
242                 break;
243             case THING_TYPE_ID_RA1570:
244                 model = RotelModel.RA1570;
245                 break;
246             case THING_TYPE_ID_RA1572:
247                 model = RotelModel.RA1572;
248                 break;
249             case THING_TYPE_ID_RA1592:
250                 if (protocol == RotelProtocol.ASCII_V1) {
251                     model = RotelModel.RA1592_V1;
252                 } else {
253                     model = RotelModel.RA1592_V2;
254                 }
255                 break;
256             case THING_TYPE_ID_RAP1580:
257                 model = RotelModel.RAP1580;
258                 break;
259             case THING_TYPE_ID_RC1570:
260                 model = RotelModel.RC1570;
261                 break;
262             case THING_TYPE_ID_RC1572:
263                 model = RotelModel.RC1572;
264                 break;
265             case THING_TYPE_ID_RC1590:
266                 if (protocol == RotelProtocol.ASCII_V1) {
267                     model = RotelModel.RC1590_V1;
268                 } else {
269                     model = RotelModel.RC1590_V2;
270                 }
271                 break;
272             case THING_TYPE_ID_RCD1570:
273                 model = RotelModel.RCD1570;
274                 break;
275             case THING_TYPE_ID_RCD1572:
276                 model = RotelModel.RCD1572;
277                 break;
278             case THING_TYPE_ID_RCX1500:
279                 model = RotelModel.RCX1500;
280                 break;
281             case THING_TYPE_ID_RDD1580:
282                 model = RotelModel.RDD1580;
283                 break;
284             case THING_TYPE_ID_RDG1520:
285             case THING_TYPE_ID_RT09:
286                 model = RotelModel.RDG1520;
287                 break;
288             case THING_TYPE_ID_RSP1576:
289                 model = RotelModel.RSP1576;
290                 break;
291             case THING_TYPE_ID_RSP1582:
292                 model = RotelModel.RSP1582;
293                 break;
294             case THING_TYPE_ID_RT11:
295                 model = RotelModel.RT11;
296                 break;
297             case THING_TYPE_ID_RT1570:
298                 model = RotelModel.RT1570;
299                 break;
300             case THING_TYPE_ID_T11:
301                 model = RotelModel.T11;
302                 break;
303             case THING_TYPE_ID_T14:
304                 model = RotelModel.T14;
305                 break;
306             case THING_TYPE_ID_C8:
307                 model = RotelModel.C8;
308                 break;
309             case THING_TYPE_ID_M8:
310                 model = RotelModel.M8;
311                 break;
312             case THING_TYPE_ID_P5:
313                 model = RotelModel.P5;
314                 break;
315             case THING_TYPE_ID_S5:
316                 model = RotelModel.S5;
317                 break;
318             case THING_TYPE_ID_X3:
319                 model = RotelModel.X3;
320                 break;
321             case THING_TYPE_ID_X5:
322                 model = RotelModel.X5;
323                 break;
324             default:
325                 model = DEFAULT_MODEL;
326                 break;
327         }
328
329         Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
330         Map<RotelSource, String> sourcesLabels = new HashMap<>();
331
332         String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
333
334         if (model.hasVolumeControl()) {
335             maxVolume = model.getVolumeMax();
336             if (!model.hasDirectVolumeControl()) {
337                 logger.info(
338                         "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
339                         minVolume, maxVolume);
340             }
341         }
342         if (model.hasToneControl()) {
343             maxToneLevel = model.getToneLevelMax();
344             minToneLevel = -maxToneLevel;
345             logger.info(
346                     "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
347                     minToneLevel, maxToneLevel);
348         }
349         if (model.hasBalanceControl()) {
350             maxBalanceLevel = model.getBalanceLevelMax();
351             minBalanceLevel = -maxBalanceLevel;
352             logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
353                     minBalanceLevel, maxBalanceLevel);
354         }
355
356         powerControlPerZone = model.hasPowerControlPerZone();
357
358         // Check configuration settings
359         String configError = null;
360         if ((config.serialPort == null || config.serialPort.isEmpty())
361                 && (config.host == null || config.host.isEmpty())) {
362             configError = "@text/offline.config-error-unknown-serialport-and-host";
363         } else if (config.host == null || config.host.isEmpty()) {
364             if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
365                 configError = "@text/offline.config-error-invalid-serial-over-ip";
366             }
367         } else {
368             if (config.port == null) {
369                 configError = "@text/offline.config-error-unknown-port";
370             } else if (config.port <= 0) {
371                 configError = "@text/offline.config-error-invalid-port";
372             }
373         }
374
375         if (configError != null) {
376             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
377         } else {
378             for (RotelSource src : model.getSources()) {
379                 // Consider custom input labels
380                 String label = null;
381                 switch (src.getName()) {
382                     case "CD":
383                         label = config.inputLabelCd;
384                         break;
385                     case "TUNER":
386                         label = config.inputLabelTuner;
387                         break;
388                     case "TAPE":
389                         label = config.inputLabelTape;
390                         break;
391                     case "PHONO":
392                         label = config.inputLabelPhono;
393                         break;
394                     case "VIDEO1":
395                         label = config.inputLabelVideo1;
396                         break;
397                     case "VIDEO2":
398                         label = config.inputLabelVideo2;
399                         break;
400                     case "VIDEO3":
401                         label = config.inputLabelVideo3;
402                         break;
403                     case "VIDEO4":
404                         label = config.inputLabelVideo4;
405                         break;
406                     case "VIDEO5":
407                         label = config.inputLabelVideo5;
408                         break;
409                     case "VIDEO6":
410                         label = config.inputLabelVideo6;
411                         break;
412                     case "USB":
413                         label = config.inputLabelUsb;
414                         break;
415                     case "MULTI":
416                         label = config.inputLabelMulti;
417                         break;
418                     default:
419                         break;
420                 }
421                 if (label != null && !label.isEmpty()) {
422                     sourcesCustomLabels.put(src, label);
423                 }
424                 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
425             }
426
427             if (protocol == RotelProtocol.HEX) {
428                 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
429             } else if (protocol == RotelProtocol.ASCII_V1) {
430                 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
431             } else {
432                 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
433             }
434
435             if (USE_SIMULATED_DEVICE) {
436                 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
437             } else if (config.serialPort != null) {
438                 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
439                         protocolHandler, readerThreadName);
440             } else {
441                 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
442             }
443
444             if (model.hasSourceControl()) {
445                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
446                         getStateOptions(model.getSources(), sourcesCustomLabels));
447                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
448                         getStateOptions(model.getSources(), sourcesCustomLabels));
449                 stateDescriptionProvider.setStateOptions(
450                         new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
451                         getStateOptions(model.getRecordSources(), sourcesCustomLabels));
452             }
453             if (model.hasZoneSourceControl(1)) {
454                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
455                         getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
456             }
457             if (model.hasZoneSourceControl(2)) {
458                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
459                         getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
460             }
461             if (model.hasZoneSourceControl(3)) {
462                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
463                         getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
464             }
465             if (model.hasZoneSourceControl(4)) {
466                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
467                         getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
468             }
469             if (model.hasDspControl()) {
470                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
471                         model.getDspStateOptions());
472                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
473                         model.getDspStateOptions());
474             }
475
476             List<CommandOption> options = model.getOtherCommandsOptions(protocol);
477             if (!options.isEmpty()) {
478                 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_OTHER_COMMAND),
479                         options);
480                 commandDescriptionProvider
481                         .setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_OTHER_COMMAND), options);
482             }
483
484             updateStatus(ThingStatus.UNKNOWN);
485
486             scheduleReconnectJob();
487         }
488
489         logger.debug("Finished initializing!");
490     }
491
492     @Override
493     public void dispose() {
494         logger.debug("Disposing handler for thing {}", getThing().getUID());
495         cancelPowerOffJob();
496         for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
497             cancelPowerOnZoneJob(zone);
498         }
499         cancelReconnectJob();
500         closeConnection();
501         super.dispose();
502     }
503
504     public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
505         List<StateOption> options = new ArrayList<>();
506         for (RotelSource item : list) {
507             String label = sourcesLabels.get(item);
508             options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
509         }
510         return options;
511     }
512
513     @Override
514     public void handleCommand(ChannelUID channelUID, Command command) {
515         String channel = channelUID.getId();
516
517         if (getThing().getStatus() != ThingStatus.ONLINE) {
518             logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
519             return;
520         }
521
522         if (command instanceof RefreshType) {
523             updateChannelState(channel);
524             return;
525         }
526
527         if (!connector.isConnected()) {
528             logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
529             return;
530         }
531
532         int numZone = 0;
533         switch (channel) {
534             case CHANNEL_ZONE1_SOURCE:
535             case CHANNEL_ZONE1_VOLUME:
536             case CHANNEL_ZONE1_MUTE:
537             case CHANNEL_ZONE1_BASS:
538             case CHANNEL_ZONE1_TREBLE:
539             case CHANNEL_ZONE1_BALANCE:
540                 numZone = 1;
541                 break;
542             case CHANNEL_ZONE2_POWER:
543             case CHANNEL_ZONE2_SOURCE:
544             case CHANNEL_ZONE2_VOLUME:
545             case CHANNEL_ZONE2_VOLUME_UP_DOWN:
546             case CHANNEL_ZONE2_MUTE:
547             case CHANNEL_ZONE2_BASS:
548             case CHANNEL_ZONE2_TREBLE:
549             case CHANNEL_ZONE2_BALANCE:
550                 numZone = 2;
551                 break;
552             case CHANNEL_ZONE3_POWER:
553             case CHANNEL_ZONE3_SOURCE:
554             case CHANNEL_ZONE3_VOLUME:
555             case CHANNEL_ZONE3_MUTE:
556             case CHANNEL_ZONE3_BASS:
557             case CHANNEL_ZONE3_TREBLE:
558             case CHANNEL_ZONE3_BALANCE:
559                 numZone = 3;
560                 break;
561             case CHANNEL_ZONE4_POWER:
562             case CHANNEL_ZONE4_SOURCE:
563             case CHANNEL_ZONE4_VOLUME:
564             case CHANNEL_ZONE4_MUTE:
565             case CHANNEL_ZONE4_BASS:
566             case CHANNEL_ZONE4_TREBLE:
567             case CHANNEL_ZONE4_BALANCE:
568                 numZone = 4;
569                 break;
570             default:
571                 break;
572         }
573
574         RotelSource src;
575         RotelCommand cmd;
576         boolean success = true;
577         synchronized (sequenceLock) {
578             try {
579                 switch (channel) {
580                     case CHANNEL_POWER:
581                     case CHANNEL_MAIN_POWER:
582                     case CHANNEL_ZONE2_POWER:
583                     case CHANNEL_ZONE3_POWER:
584                     case CHANNEL_ZONE4_POWER:
585                         if (numZone == 0 || model.hasZoneCommands(numZone)) {
586                             handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
587                         } else if (numZone == 2 && model.getNumberOfZones() == 2) {
588                             if (isPowerOn() || isPowerOn(numZone)) {
589                                 selectZone(2, model.getZoneSelectCmd());
590                             }
591                             sendCommand(RotelCommand.ZONE_SELECT);
592                         } else {
593                             success = false;
594                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
595                         }
596                         break;
597                     case CHANNEL_ALL_POWER:
598                         handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
599                         break;
600                     case CHANNEL_SOURCE:
601                     case CHANNEL_MAIN_SOURCE:
602                     case CHANNEL_ZONE1_SOURCE:
603                     case CHANNEL_ZONE2_SOURCE:
604                     case CHANNEL_ZONE3_SOURCE:
605                     case CHANNEL_ZONE4_SOURCE:
606                         if (!isPowerOn(numZone)) {
607                             success = false;
608                             logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
609                                     numZone == 0 ? "device" : "zone " + numZone);
610                         } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
611                             src = model.getSourceFromName(command.toString());
612                             if (numZone == 0) {
613                                 cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
614                             } else {
615                                 cmd = src.getZoneCommand(numZone);
616                             }
617                             if (cmd != null) {
618                                 sendCommand(cmd);
619                                 if (model.canGetFrequency()) {
620                                     // send <new-source> returns
621                                     // 1.) the selected <new-source>
622                                     // 2.) the used frequency
623                                     // BUT:
624                                     // at response-time the frequency has the value of <old-source>
625                                     // so we must wait a short moment to get the frequency of <new-source>
626                                     Thread.sleep(1000);
627                                     sendCommand(RotelCommand.FREQUENCY);
628                                     Thread.sleep(100);
629                                     updateChannelState(CHANNEL_FREQUENCY);
630                                 }
631                             } else {
632                                 success = false;
633                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
634                                         channel);
635                             }
636                         } else if (numZone == 2 && model.getNumberOfZones() > 1) {
637                             src = model.getSourceFromName(command.toString());
638                             cmd = src.getCommand();
639                             if (cmd != null) {
640                                 selectZone(2, model.getZoneSelectCmd());
641                                 sendCommand(cmd);
642                                 if (model.canGetFrequency()) {
643                                     // send <new-source> returns
644                                     // 1.) the selected <new-source>
645                                     // 2.) the used frequency
646                                     // BUT:
647                                     // at response-time the frequency has the value of <old-source>
648                                     // so we must wait a short moment to get the frequency of <new-source>
649                                     Thread.sleep(1000);
650                                     sendCommand(RotelCommand.FREQUENCY);
651                                     Thread.sleep(100);
652                                     updateChannelState(CHANNEL_FREQUENCY);
653                                 }
654                             } else {
655                                 success = false;
656                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
657                                         channel);
658                             }
659                         } else {
660                             success = false;
661                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
662                         }
663                         break;
664                     case CHANNEL_MAIN_RECORD_SOURCE:
665                         if (!isPowerOn()) {
666                             success = false;
667                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
668                         } else if (model.hasOtherThanPrimaryCommands()) {
669                             src = model.getSourceFromName(command.toString());
670                             cmd = src.getRecordCommand();
671                             if (cmd != null) {
672                                 sendCommand(cmd);
673                             } else {
674                                 success = false;
675                                 logger.debug("Command {} from channel {} failed: undefined record source command",
676                                         command, channel);
677                             }
678                         } else {
679                             src = model.getSourceFromName(command.toString());
680                             cmd = src.getCommand();
681                             if (cmd != null) {
682                                 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
683                                 Thread.sleep(100);
684                                 sendCommand(cmd);
685                             } else {
686                                 success = false;
687                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
688                                         channel);
689                             }
690                         }
691                         break;
692                     case CHANNEL_DSP:
693                     case CHANNEL_MAIN_DSP:
694                         if (!isPowerOn()) {
695                             success = false;
696                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
697                         } else {
698                             sendCommand(model.getCommandFromDspName(command.toString()));
699                         }
700                         break;
701                     case CHANNEL_VOLUME:
702                     case CHANNEL_MAIN_VOLUME:
703                     case CHANNEL_MAIN_VOLUME_UP_DOWN:
704                     case CHANNEL_ZONE1_VOLUME:
705                     case CHANNEL_ZONE2_VOLUME:
706                     case CHANNEL_ZONE2_VOLUME_UP_DOWN:
707                     case CHANNEL_ZONE3_VOLUME:
708                     case CHANNEL_ZONE4_VOLUME:
709                         if (!isPowerOn(numZone)) {
710                             success = false;
711                             logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
712                                     numZone == 0 ? "device" : "zone " + numZone);
713                         } else if (fixedVolumeZones[numZone]) {
714                             success = false;
715                             logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
716                         } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
717                             handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
718                                     getVolumeDownCommand(numZone),
719                                     CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
720                                             || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
721                                                     : getVolumeSetCommand(numZone));
722                         } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
723                             selectZone(2, model.getZoneSelectCmd());
724                             handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
725                                     RotelCommand.VOLUME_DOWN,
726                                     CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
727                         } else {
728                             success = false;
729                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
730                         }
731                         break;
732                     case CHANNEL_MUTE:
733                     case CHANNEL_MAIN_MUTE:
734                     case CHANNEL_ZONE1_MUTE:
735                     case CHANNEL_ZONE2_MUTE:
736                     case CHANNEL_ZONE3_MUTE:
737                     case CHANNEL_ZONE4_MUTE:
738                         if (!isPowerOn(numZone)) {
739                             success = false;
740                             logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
741                                     numZone == 0 ? "device" : "zone " + numZone);
742                         } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
743                             handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
744                                     getMuteOnCommand(numZone), getMuteOffCommand(numZone),
745                                     getMuteToggleCommand(numZone));
746                         } else {
747                             success = false;
748                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
749                         }
750                         break;
751                     case CHANNEL_BASS:
752                     case CHANNEL_MAIN_BASS:
753                     case CHANNEL_ZONE1_BASS:
754                     case CHANNEL_ZONE2_BASS:
755                     case CHANNEL_ZONE3_BASS:
756                     case CHANNEL_ZONE4_BASS:
757                         if (!isPowerOn(numZone)) {
758                             success = false;
759                             logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
760                                     numZone == 0 ? "device" : "zone " + numZone);
761                         } else if (tcbypass) {
762                             success = false;
763                             logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
764                                     channel);
765                         } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
766                             handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
767                                     getBassDownCommand(numZone), getBassSetCommand(numZone));
768                         } else {
769                             success = false;
770                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
771                         }
772                         break;
773                     case CHANNEL_TREBLE:
774                     case CHANNEL_MAIN_TREBLE:
775                     case CHANNEL_ZONE1_TREBLE:
776                     case CHANNEL_ZONE2_TREBLE:
777                     case CHANNEL_ZONE3_TREBLE:
778                     case CHANNEL_ZONE4_TREBLE:
779                         if (!isPowerOn(numZone)) {
780                             success = false;
781                             logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
782                                     numZone == 0 ? "device" : "zone " + numZone);
783                         } else if (tcbypass) {
784                             success = false;
785                             logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
786                                     channel);
787                         } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
788                             handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
789                                     getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
790                         } else {
791                             success = false;
792                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
793                         }
794                         break;
795                     case CHANNEL_PLAY_CONTROL:
796                         if (!isPowerOn()) {
797                             success = false;
798                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
799                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
800                             sendCommand(RotelCommand.PLAY);
801                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
802                             sendCommand(RotelCommand.PAUSE);
803                             if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
804                                     && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
805                                 Thread.sleep(SLEEP_INTV);
806                                 sendCommand(RotelCommand.PLAY_STATUS);
807                             }
808                         } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
809                             sendCommand(RotelCommand.TRACK_FWD);
810                         } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
811                             sendCommand(RotelCommand.TRACK_BACK);
812                         } else {
813                             success = false;
814                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
815                         }
816                         break;
817                     case CHANNEL_RANDOM:
818                         if (!isPowerOn()) {
819                             success = false;
820                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
821                         } else if (command instanceof OnOffType) {
822                             sendCommand(RotelCommand.RANDOM_TOGGLE);
823                         } else {
824                             success = false;
825                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
826                         }
827                         break;
828                     case CHANNEL_REPEAT:
829                         if (!isPowerOn()) {
830                             success = false;
831                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
832                         } else {
833                             RotelRepeatMode currentMode = repeatMode;
834                             RotelRepeatMode mode = RotelRepeatMode.OFF;
835                             try {
836                                 mode = RotelRepeatMode.getFromName(command.toString());
837                                 if (mode == currentMode) {
838                                     success = false;
839                                     logger.debug("Command {} from channel {} ignored: no change requested", command,
840                                             channel);
841                                 }
842                             } catch (RotelException e) {
843                                 success = false;
844                                 logger.debug("Command {} from channel {} failed: invalid command value", command,
845                                         channel);
846                             }
847                             if (success) {
848                                 // Toggle TRACK -> DISC -> OFF
849                                 sendCommand(RotelCommand.REPEAT_TOGGLE);
850                                 if ((mode == RotelRepeatMode.OFF && currentMode == RotelRepeatMode.TRACK)
851                                         || (mode == RotelRepeatMode.TRACK && currentMode == RotelRepeatMode.DISC)
852                                         || (mode == RotelRepeatMode.DISC && currentMode == RotelRepeatMode.OFF)) {
853                                     Thread.sleep(SLEEP_INTV);
854                                     sendCommand(RotelCommand.REPEAT_TOGGLE);
855                                 }
856                             }
857                         }
858                         break;
859                     case CHANNEL_RADIO_PRESET:
860                         if (!isPowerOn()) {
861                             success = false;
862                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
863                         } else {
864                             int value = 0;
865                             if (radioPreset > 0 && command instanceof IncreaseDecreaseType
866                                     && command == IncreaseDecreaseType.INCREASE) {
867                                 value = radioPreset + 1;
868                             } else if (radioPreset > 0 && command instanceof IncreaseDecreaseType
869                                     && command == IncreaseDecreaseType.DECREASE) {
870                                 value = radioPreset - 1;
871                             } else if (command instanceof DecimalType) {
872                                 value = ((DecimalType) command).intValue();
873                             }
874                             if (value >= 1 && value <= 30) {
875                                 RotelSource source = sources[0];
876                                 RotelCommand presetCallCmd = source == null ? null : getRadioPresetCallCommand(source);
877                                 if (presetCallCmd != null) {
878                                     sendCommand(presetCallCmd, value);
879                                     // In ASCII V2, the previous command will return nothing
880                                     RotelCommand presetGetCmd = source == null ? null
881                                             : getRadioPresetGetCommand(source);
882                                     if (protocol == RotelProtocol.ASCII_V2 && presetGetCmd != null) {
883                                         Thread.sleep(SLEEP_INTV);
884                                         sendCommand(presetGetCmd);
885                                     }
886                                 } else {
887                                     success = false;
888                                     logger.debug("Command {} from channel {} ignored: current source is not radio",
889                                             command, channel);
890                                 }
891                             } else {
892                                 success = false;
893                                 logger.debug("Command {} from channel {} ignored: value out of bounds", command,
894                                         channel);
895                             }
896                         }
897                         break;
898                     case CHANNEL_BRIGHTNESS:
899                     case CHANNEL_ALL_BRIGHTNESS:
900                         if (!isPowerOn()) {
901                             success = false;
902                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
903                         } else if (!model.hasDimmerControl()) {
904                             success = false;
905                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
906                         } else if (command instanceof PercentType) {
907                             int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
908                                     * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
909                                     + model.getDimmerLevelMin();
910                             sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
911                         } else {
912                             success = false;
913                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
914                         }
915                         break;
916                     case CHANNEL_TCBYPASS:
917                         if (!isPowerOn()) {
918                             success = false;
919                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
920                         } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
921                             success = false;
922                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
923                         } else {
924                             handleTcbypassCmd(channel, command,
925                                     protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
926                                             : RotelCommand.TCBYPASS_ON,
927                                     protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
928                                             : RotelCommand.TCBYPASS_OFF);
929                         }
930                         break;
931                     case CHANNEL_BALANCE:
932                     case CHANNEL_ZONE1_BALANCE:
933                     case CHANNEL_ZONE2_BALANCE:
934                     case CHANNEL_ZONE3_BALANCE:
935                     case CHANNEL_ZONE4_BALANCE:
936                         if (!isPowerOn(numZone)) {
937                             success = false;
938                             logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
939                                     numZone == 0 ? "device" : "zone " + numZone);
940                         } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
941                             success = false;
942                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
943                         } else {
944                             handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
945                                     getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
946                         }
947                         break;
948                     case CHANNEL_SPEAKER_A:
949                         if (!isPowerOn()) {
950                             success = false;
951                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
952                         } else {
953                             handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
954                                     RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
955                         }
956                         break;
957                     case CHANNEL_SPEAKER_B:
958                         if (!isPowerOn()) {
959                             success = false;
960                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
961                         } else {
962                             handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
963                                     RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
964                         }
965                         break;
966                     case CHANNEL_OTHER_COMMAND:
967                     case CHANNEL_MAIN_OTHER_COMMAND:
968                         if (!isPowerOn()) {
969                             success = false;
970                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
971                         } else {
972                             try {
973                                 cmd = RotelCommand.getFromName(command.toString());
974                             } catch (RotelException e) {
975                                 success = false;
976                                 logger.debug("Command {} from channel {} failed: undefined command", command, channel);
977                                 cmd = null;
978                             }
979                             if (cmd != null) {
980                                 sendCommand(cmd);
981                             }
982                         }
983                         break;
984                     default:
985                         success = false;
986                         logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
987                         break;
988                 }
989                 if (success) {
990                     logger.debug("Command {} from channel {} succeeded", command, channel);
991                 } else {
992                     updateChannelState(channel);
993                 }
994             } catch (RotelException e) {
995                 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
996                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
997                         "@text/offline.comm-error-sending-command");
998                 closeConnection();
999                 scheduleReconnectJob();
1000             } catch (InterruptedException e) {
1001                 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
1002                 Thread.currentThread().interrupt();
1003             }
1004         }
1005     }
1006
1007     /**
1008      * Handle a power ON/OFF command
1009      *
1010      * @param channel the channel
1011      * @param command the received channel command (OnOffType)
1012      * @param onCmd the command to be sent to the device to power it ON
1013      * @param offCmd the command to be sent to the device to power it OFF
1014      *
1015      * @throws RotelException in case of communication error with the device
1016      */
1017     private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1018             throws RotelException {
1019         if (command instanceof OnOffType && command == OnOffType.ON) {
1020             sendCommand(onCmd);
1021         } else if (command instanceof OnOffType && command == OnOffType.OFF) {
1022             sendCommand(offCmd);
1023         } else {
1024             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1025         }
1026     }
1027
1028     /**
1029      * Handle a volume command
1030      *
1031      * @param current the current volume
1032      * @param channel the channel
1033      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1034      * @param upCmd the command to be sent to the device to increase the volume
1035      * @param downCmd the command to be sent to the device to decrease the volume
1036      * @param setCmd the command to be sent to the device to set the volume at a value
1037      *
1038      * @throws RotelException in case of communication error with the device
1039      */
1040     private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
1041             @Nullable RotelCommand setCmd) throws RotelException {
1042         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1043             sendCommand(upCmd);
1044         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1045             sendCommand(downCmd);
1046         } else if (command instanceof DecimalType && setCmd == null) {
1047             int value = ((DecimalType) command).intValue();
1048             if (value >= minVolume && value <= maxVolume) {
1049                 if (value > current) {
1050                     sendCommand(upCmd);
1051                 } else if (value < current) {
1052                     sendCommand(downCmd);
1053                 }
1054             }
1055         } else if (command instanceof PercentType && setCmd != null) {
1056             int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1057                     + minVolume;
1058             sendCommand(setCmd, value);
1059         } else {
1060             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1061         }
1062     }
1063
1064     /**
1065      * Handle a mute command
1066      *
1067      * @param onlyToggle true if only the toggle command must be used
1068      * @param channel the channel
1069      * @param command the received channel command (OnOffType)
1070      * @param onCmd the command to be sent to the device to mute
1071      * @param offCmd the command to be sent to the device to unmute
1072      * @param toggleCmd the command to be sent to the device to toggle the mute state
1073      *
1074      * @throws RotelException in case of communication error with the device
1075      */
1076     private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1077             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1078         if (command instanceof OnOffType) {
1079             if (onlyToggle) {
1080                 sendCommand(toggleCmd);
1081             } else if (command == OnOffType.ON) {
1082                 sendCommand(onCmd);
1083             } else if (command == OnOffType.OFF) {
1084                 sendCommand(offCmd);
1085             }
1086         } else {
1087             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1088         }
1089     }
1090
1091     /**
1092      * Handle a tone level adjustment command (bass or treble)
1093      *
1094      * @param current the current tone level
1095      * @param channel the channel
1096      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1097      * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1098      * @param upCmd the command to be sent to the device to increase the tone level
1099      * @param downCmd the command to be sent to the device to decrease the tone level
1100      * @param setCmd the command to be sent to the device to set the tone level at a value
1101      *
1102      * @throws RotelException in case of communication error with the device
1103      * @throws InterruptedException in case of interruption during a thread sleep
1104      */
1105     private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1106             RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1107         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1108             selectToneControl(nbSelect);
1109             sendCommand(upCmd);
1110         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1111             selectToneControl(nbSelect);
1112             sendCommand(downCmd);
1113         } else if (command instanceof DecimalType) {
1114             int value = ((DecimalType) command).intValue();
1115             if (value >= minToneLevel && value <= maxToneLevel) {
1116                 if (protocol != RotelProtocol.HEX) {
1117                     sendCommand(setCmd, value);
1118                 } else if (value > current) {
1119                     selectToneControl(nbSelect);
1120                     sendCommand(upCmd);
1121                 } else if (value < current) {
1122                     selectToneControl(nbSelect);
1123                     sendCommand(downCmd);
1124                 }
1125             }
1126         } else {
1127             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1128         }
1129     }
1130
1131     /**
1132      * Handle a tcbypass command (only for ASCII protocol)
1133      *
1134      * @param channel the channel
1135      * @param command the received channel command (OnOffType)
1136      * @param onCmd the command to be sent to the device to bypass_on
1137      * @param offCmd the command to be sent to the device to bypass_off
1138      *
1139      * @throws RotelException in case of communication error with the device
1140      */
1141     private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1142             throws RotelException, InterruptedException {
1143         if (command instanceof OnOffType) {
1144             if (command == OnOffType.ON) {
1145                 sendCommand(onCmd);
1146                 basses[0] = 0;
1147                 trebles[0] = 0;
1148                 updateChannelState(CHANNEL_BASS);
1149                 updateChannelState(CHANNEL_TREBLE);
1150             } else if (command == OnOffType.OFF) {
1151                 sendCommand(offCmd);
1152                 Thread.sleep(200);
1153                 sendCommand(RotelCommand.BASS);
1154                 Thread.sleep(200);
1155                 sendCommand(RotelCommand.TREBLE);
1156             }
1157         } else {
1158             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1159         }
1160     }
1161
1162     /**
1163      * Handle a speaker command
1164      *
1165      * @param onlyToggle true if only the toggle command must be used
1166      * @param channel the channel
1167      * @param command the received channel command (OnOffType)
1168      * @param onCmd the command to be sent to the device to speaker_x_on
1169      * @param offCmd the command to be sent to the device to speaker_x_off
1170      * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1171      *
1172      * @throws RotelException in case of communication error with the device
1173      */
1174     private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1175             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1176         if (command instanceof OnOffType) {
1177             if (onlyToggle) {
1178                 sendCommand(toggleCmd);
1179             } else if (command == OnOffType.ON) {
1180                 sendCommand(onCmd);
1181             } else if (command == OnOffType.OFF) {
1182                 sendCommand(offCmd);
1183             }
1184         } else {
1185             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1186         }
1187     }
1188
1189     /**
1190      * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1191      *
1192      * @param channel the channel
1193      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1194      * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1195      * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1196      * @param setCmd the command to be sent to the device to set the balance at a value
1197      *
1198      * @throws RotelException in case of communication error with the device
1199      * @throws InterruptedException in case of interruption during a thread sleep
1200      */
1201     private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1202             RotelCommand setCmd) throws RotelException, InterruptedException {
1203         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1204             sendCommand(rightCmd);
1205         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1206             sendCommand(leftCmd);
1207         } else if (command instanceof DecimalType) {
1208             int value = ((DecimalType) command).intValue();
1209             if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1210                 sendCommand(setCmd, value);
1211             }
1212         } else {
1213             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1214         }
1215     }
1216
1217     /**
1218      * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1219      *
1220      * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1221      *
1222      * @throws RotelException in case of communication error with the device
1223      * @throws InterruptedException in case of interruption during a thread sleep
1224      */
1225     private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1226         // No tone control select command for RSX-1065
1227         if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1228             selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1229         }
1230     }
1231
1232     /**
1233      * Run a sequence of commands to display a particular zone on the device front panel
1234      *
1235      * @param zone the zone to be displayed (1 for main zone)
1236      * @param selectCommand the command to be sent to the device to switch the display between zones
1237      *
1238      * @throws RotelException in case of communication error with the device
1239      * @throws InterruptedException in case of interruption during a thread sleep
1240      */
1241     private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1242             throws RotelException, InterruptedException {
1243         if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
1244                 && selectCommand != null) {
1245             int nbSelect;
1246             if (zone < currentZone) {
1247                 nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
1248                 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1249                     nbSelect++;
1250                 }
1251             } else {
1252                 nbSelect = zone - currentZone;
1253                 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1254                         && !selectingRecord) {
1255                     nbSelect++;
1256                 }
1257             }
1258             selectFeature(nbSelect, null, selectCommand);
1259         }
1260     }
1261
1262     /**
1263      * Run a sequence of commands to display a particular feature on the device front panel
1264      *
1265      * @param nbSelect the number of select commands to be run
1266      * @param preCmd the initial command to be sent to the device (before the select commands)
1267      * @param selectCmd the select command to be sent to the device
1268      *
1269      * @throws RotelException in case of communication error with the device
1270      * @throws InterruptedException in case of interruption during a thread sleep
1271      */
1272     private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1273             throws RotelException, InterruptedException {
1274         if (protocol == RotelProtocol.HEX) {
1275             if (preCmd != null) {
1276                 sendCommand(preCmd);
1277                 Thread.sleep(100);
1278             }
1279             for (int i = 1; i <= nbSelect; i++) {
1280                 sendCommand(selectCmd);
1281                 Thread.sleep(200);
1282             }
1283         }
1284     }
1285
1286     /**
1287      * Open the connection with the Rotel device
1288      *
1289      * @return true if the connection is opened successfully or flase if not
1290      */
1291     private synchronized boolean openConnection() {
1292         protocolHandler.addEventListener(this);
1293         try {
1294             connector.open();
1295         } catch (RotelException e) {
1296             logger.debug("openConnection() failed", e);
1297         }
1298         logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1299         return connector.isConnected();
1300     }
1301
1302     /**
1303      * Close the connection with the Rotel device
1304      */
1305     private synchronized void closeConnection() {
1306         connector.close();
1307         protocolHandler.removeEventListener(this);
1308         logger.debug("closeConnection(): disconnected");
1309     }
1310
1311     @Override
1312     public void onNewMessageEvent(EventObject event) {
1313         cancelPowerOffJob();
1314
1315         RotelMessageEvent evt = (RotelMessageEvent) event;
1316         logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1317
1318         String key = evt.getKey();
1319         String value = evt.getValue().trim();
1320         if (!KEY_ERROR.equals(key)) {
1321             updateStatus(ThingStatus.ONLINE);
1322         }
1323         int numZone = 0;
1324         switch (key) {
1325             case KEY_INPUT_ZONE1:
1326             case KEY_VOLUME_ZONE1:
1327             case KEY_MUTE_ZONE1:
1328             case KEY_BASS_ZONE1:
1329             case KEY_TREBLE_ZONE1:
1330             case KEY_BALANCE_ZONE1:
1331             case KEY_FREQ_ZONE1:
1332                 numZone = 1;
1333                 break;
1334             case KEY_POWER_ZONE2:
1335             case KEY_SOURCE_ZONE2:
1336             case KEY_INPUT_ZONE2:
1337             case KEY_VOLUME_ZONE2:
1338             case KEY_MUTE_ZONE2:
1339             case KEY_BASS_ZONE2:
1340             case KEY_TREBLE_ZONE2:
1341             case KEY_BALANCE_ZONE2:
1342             case KEY_FREQ_ZONE2:
1343                 numZone = 2;
1344                 break;
1345             case KEY_POWER_ZONE3:
1346             case KEY_SOURCE_ZONE3:
1347             case KEY_INPUT_ZONE3:
1348             case KEY_VOLUME_ZONE3:
1349             case KEY_MUTE_ZONE3:
1350             case KEY_BASS_ZONE3:
1351             case KEY_TREBLE_ZONE3:
1352             case KEY_BALANCE_ZONE3:
1353             case KEY_FREQ_ZONE3:
1354                 numZone = 3;
1355                 break;
1356             case KEY_POWER_ZONE4:
1357             case KEY_SOURCE_ZONE4:
1358             case KEY_INPUT_ZONE4:
1359             case KEY_VOLUME_ZONE4:
1360             case KEY_MUTE_ZONE4:
1361             case KEY_BASS_ZONE4:
1362             case KEY_TREBLE_ZONE4:
1363             case KEY_BALANCE_ZONE4:
1364             case KEY_FREQ_ZONE4:
1365                 numZone = 4;
1366                 break;
1367             default:
1368                 break;
1369         }
1370         int preset = 0;
1371         if (key.startsWith(KEY_FM_PRESET)) {
1372             try {
1373                 preset = Integer.parseInt(key.substring(KEY_FM_PRESET.length()));
1374             } catch (NumberFormatException e) {
1375                 // Considering the Rotel protocol, the parsing could not fail in practice.
1376                 // In case it would fail, 0 will be considered as preset, meaning undefined.
1377             }
1378             key = KEY_FM_PRESET;
1379         } else if (key.startsWith(KEY_DAB_PRESET)) {
1380             try {
1381                 preset = Integer.parseInt(key.substring(KEY_DAB_PRESET.length()));
1382             } catch (NumberFormatException e) {
1383                 // Considering the Rotel protocol, the parsing could not fail in practice.
1384                 // In case it would fail, 0 will be considered as preset, meaning undefined.
1385             }
1386             key = KEY_DAB_PRESET;
1387         } else if (key.startsWith(KEY_IRADIO_PRESET)) {
1388             try {
1389                 preset = Integer.parseInt(key.substring(KEY_IRADIO_PRESET.length()));
1390             } catch (NumberFormatException e) {
1391                 // Considering the Rotel protocol, the parsing could not fail in practice.
1392                 // In case it would fail, 0 will be considered as preset, meaning undefined.
1393             }
1394             key = KEY_IRADIO_PRESET;
1395         }
1396         RotelSource source;
1397         try {
1398             switch (key) {
1399                 case KEY_ERROR:
1400                     logger.debug("Reading feedback message failed");
1401                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1402                             "@text/offline.comm-error-reading-thread");
1403                     closeConnection();
1404                     break;
1405                 case KEY_LINE1:
1406                     frontPanelLine1 = value;
1407                     updateChannelState(CHANNEL_LINE1);
1408                     break;
1409                 case KEY_LINE2:
1410                     frontPanelLine2 = value;
1411                     updateChannelState(CHANNEL_LINE2);
1412                     break;
1413                 case KEY_ZONE:
1414                     currentZone = Integer.parseInt(value);
1415                     break;
1416                 case KEY_RECORD_SEL:
1417                     selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1418                     break;
1419                 case KEY_POWER:
1420                     if (POWER_ON.equalsIgnoreCase(value)) {
1421                         handlePowerOn();
1422                     } else if (STANDBY.equalsIgnoreCase(value)) {
1423                         handlePowerOff();
1424                         if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
1425                             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1426                                 handlePowerOffZone(zone);
1427                             }
1428                         }
1429                     } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1430                         schedulePowerOffJob(false);
1431                     } else {
1432                         throw new RotelException("Invalid value");
1433                     }
1434                     break;
1435                 case KEY_POWER_ZONE2:
1436                 case KEY_POWER_ZONE3:
1437                 case KEY_POWER_ZONE4:
1438                     if (POWER_ON.equalsIgnoreCase(value)) {
1439                         handlePowerOnZone(numZone);
1440                     } else if (STANDBY.equalsIgnoreCase(value)) {
1441                         handlePowerOffZone(numZone);
1442                     } else {
1443                         throw new RotelException("Invalid value");
1444                     }
1445                     break;
1446                 case KEY_POWER_MODE:
1447                     logger.debug("Power mode is set to {}", value);
1448                     break;
1449                 case KEY_VOLUME_MIN:
1450                     minVolume = Integer.parseInt(value);
1451                     if (!model.hasDirectVolumeControl()) {
1452                         logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1453                                 minVolume);
1454                     }
1455                     break;
1456                 case KEY_VOLUME_MAX:
1457                     maxVolume = Integer.parseInt(value);
1458                     if (!model.hasDirectVolumeControl()) {
1459                         logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1460                                 maxVolume);
1461                     }
1462                     break;
1463                 case KEY_VOLUME:
1464                 case KEY_VOLUME_ZONE1:
1465                 case KEY_VOLUME_ZONE2:
1466                 case KEY_VOLUME_ZONE3:
1467                 case KEY_VOLUME_ZONE4:
1468                     fixedVolumeZones[numZone] = false;
1469                     if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1470                         fixedVolumeZones[numZone] = true;
1471                     } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1472                         volumes[numZone] = minVolume;
1473                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1474                         volumes[numZone] = maxVolume;
1475                     } else {
1476                         volumes[numZone] = Integer.parseInt(value);
1477                     }
1478                     if (numZone == 0) {
1479                         updateChannelState(CHANNEL_VOLUME);
1480                         updateChannelState(CHANNEL_MAIN_VOLUME);
1481                         updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1482                     } else {
1483                         updateGroupChannelState(numZone, CHANNEL_VOLUME);
1484                         updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1485                     }
1486                     break;
1487                 case KEY_MUTE:
1488                 case KEY_MUTE_ZONE1:
1489                 case KEY_MUTE_ZONE2:
1490                 case KEY_MUTE_ZONE3:
1491                 case KEY_MUTE_ZONE4:
1492                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1493                         mutes[numZone] = true;
1494                         if (numZone == 0) {
1495                             updateChannelState(CHANNEL_MUTE);
1496                             updateChannelState(CHANNEL_MAIN_MUTE);
1497                         } else {
1498                             updateGroupChannelState(numZone, CHANNEL_MUTE);
1499                         }
1500                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1501                         mutes[numZone] = false;
1502                         if (numZone == 0) {
1503                             updateChannelState(CHANNEL_MUTE);
1504                             updateChannelState(CHANNEL_MAIN_MUTE);
1505                         } else {
1506                             updateGroupChannelState(numZone, CHANNEL_MUTE);
1507                         }
1508                     } else {
1509                         throw new RotelException("Invalid value");
1510                     }
1511                     break;
1512                 case KEY_TONE_MAX:
1513                     maxToneLevel = Integer.parseInt(value);
1514                     minToneLevel = -maxToneLevel;
1515                     logger.info(
1516                             "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1517                             minToneLevel, maxToneLevel);
1518                     break;
1519                 case KEY_BASS:
1520                 case KEY_BASS_ZONE1:
1521                 case KEY_BASS_ZONE2:
1522                 case KEY_BASS_ZONE3:
1523                 case KEY_BASS_ZONE4:
1524                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1525                         basses[numZone] = minToneLevel;
1526                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1527                         basses[numZone] = maxToneLevel;
1528                     } else {
1529                         basses[numZone] = Integer.parseInt(value);
1530                     }
1531                     if (numZone == 0) {
1532                         updateChannelState(CHANNEL_BASS);
1533                         updateChannelState(CHANNEL_MAIN_BASS);
1534                     } else {
1535                         updateGroupChannelState(numZone, CHANNEL_BASS);
1536                     }
1537                     break;
1538                 case KEY_TREBLE:
1539                 case KEY_TREBLE_ZONE1:
1540                 case KEY_TREBLE_ZONE2:
1541                 case KEY_TREBLE_ZONE3:
1542                 case KEY_TREBLE_ZONE4:
1543                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1544                         trebles[numZone] = minToneLevel;
1545                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1546                         trebles[numZone] = maxToneLevel;
1547                     } else {
1548                         trebles[numZone] = Integer.parseInt(value);
1549                     }
1550                     if (numZone == 0) {
1551                         updateChannelState(CHANNEL_TREBLE);
1552                         updateChannelState(CHANNEL_MAIN_TREBLE);
1553                     } else {
1554                         updateGroupChannelState(numZone, CHANNEL_TREBLE);
1555                     }
1556                     break;
1557                 case KEY_SOURCE:
1558                     source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1559                     sources[0] = source;
1560                     updateChannelState(CHANNEL_SOURCE);
1561                     updateChannelState(CHANNEL_MAIN_SOURCE);
1562                     RotelCommand presetGetCmd = getRadioPresetGetCommand(source);
1563                     if (presetGetCmd != null) {
1564                         // Request current preset (with a delay)
1565                         scheduler.schedule(() -> {
1566                             try {
1567                                 sendCommand(presetGetCmd);
1568                             } catch (RotelException e) {
1569                                 logger.debug("Getting the radio preset failed: {}", e.getMessage());
1570                             }
1571                         }, 250, TimeUnit.MILLISECONDS);
1572                     } else {
1573                         radioPreset = 0;
1574                         updateChannelState(CHANNEL_RADIO_PRESET);
1575                     }
1576                     break;
1577                 case KEY_RECORD:
1578                     recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1579                     updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1580                     break;
1581                 case KEY_SOURCE_ZONE2:
1582                 case KEY_SOURCE_ZONE3:
1583                 case KEY_SOURCE_ZONE4:
1584                 case KEY_INPUT_ZONE1:
1585                 case KEY_INPUT_ZONE2:
1586                 case KEY_INPUT_ZONE3:
1587                 case KEY_INPUT_ZONE4:
1588                     sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
1589                     updateGroupChannelState(numZone, CHANNEL_SOURCE);
1590                     break;
1591                 case KEY_DSP_MODE:
1592                     if ("dolby_pliix_movie".equals(value)) {
1593                         value = "dolby_plii_movie";
1594                     } else if ("dolby_pliix_music".equals(value)) {
1595                         value = "dolby_plii_music";
1596                     } else if ("dolby_pliix_game".equals(value)) {
1597                         value = "dolby_plii_game";
1598                     }
1599                     dsp = model.getDspFromFeedback(value);
1600                     logger.debug("DSP {}", dsp.getName());
1601                     updateChannelState(CHANNEL_DSP);
1602                     updateChannelState(CHANNEL_MAIN_DSP);
1603                     break;
1604                 case KEY1_PLAY_STATUS:
1605                 case KEY2_PLAY_STATUS:
1606                     if (PLAY.equalsIgnoreCase(value)) {
1607                         playStatus = RotelPlayStatus.PLAYING;
1608                         updateChannelState(CHANNEL_PLAY_CONTROL);
1609                     } else if (PAUSE.equalsIgnoreCase(value)) {
1610                         playStatus = RotelPlayStatus.PAUSED;
1611                         updateChannelState(CHANNEL_PLAY_CONTROL);
1612                     } else if (STOP.equalsIgnoreCase(value)) {
1613                         playStatus = RotelPlayStatus.STOPPED;
1614                         updateChannelState(CHANNEL_PLAY_CONTROL);
1615                     } else {
1616                         throw new RotelException("Invalid value");
1617                     }
1618                     break;
1619                 case KEY_TRACK:
1620                     source = sources[0];
1621                     if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1622                         track = Integer.parseInt(value);
1623                         updateChannelState(CHANNEL_TRACK);
1624                     }
1625                     break;
1626                 case KEY_RANDOM:
1627                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1628                         randomMode = true;
1629                         updateChannelState(CHANNEL_RANDOM);
1630                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1631                         randomMode = false;
1632                         updateChannelState(CHANNEL_RANDOM);
1633                     } else {
1634                         throw new RotelException("Invalid value");
1635                     }
1636                     break;
1637                 case KEY_REPEAT:
1638                     if (TRACK.equalsIgnoreCase(value)) {
1639                         repeatMode = RotelRepeatMode.TRACK;
1640                         updateChannelState(CHANNEL_REPEAT);
1641                     } else if (DISC.equalsIgnoreCase(value)) {
1642                         repeatMode = RotelRepeatMode.DISC;
1643                         updateChannelState(CHANNEL_REPEAT);
1644                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1645                         repeatMode = RotelRepeatMode.OFF;
1646                         updateChannelState(CHANNEL_REPEAT);
1647                     } else {
1648                         throw new RotelException("Invalid value");
1649                     }
1650                     break;
1651                 case KEY_PRESET_FM:
1652                 case KEY_PRESET_DAB:
1653                 case KEY_PRESET_IRADIO:
1654                     preset = Integer.parseInt(value);
1655                 case KEY_FM_PRESET:
1656                 case KEY_DAB_PRESET:
1657                 case KEY_IRADIO_PRESET:
1658                     if (preset >= 1 && preset <= 30) {
1659                         radioPreset = preset;
1660                     } else {
1661                         radioPreset = 0;
1662                     }
1663                     updateChannelState(CHANNEL_RADIO_PRESET);
1664                     break;
1665                 case KEY_FM:
1666                 case KEY_DAB:
1667                     preset = Integer.parseInt(value);
1668                     if (preset >= 1 && preset <= 30) {
1669                         radioPreset = preset;
1670                         updateChannelState(CHANNEL_RADIO_PRESET);
1671                     }
1672                     break;
1673                 case KEY_FREQ:
1674                 case KEY_FREQ_ZONE1:
1675                 case KEY_FREQ_ZONE2:
1676                 case KEY_FREQ_ZONE3:
1677                 case KEY_FREQ_ZONE4:
1678                     if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
1679                         frequencies[numZone] = 0.0;
1680                     } else {
1681                         // Suppress a potential ending "k" or "K"
1682                         if (value.toUpperCase().endsWith("K")) {
1683                             value = value.substring(0, value.length() - 1);
1684                         }
1685                         frequencies[numZone] = Double.parseDouble(value);
1686                     }
1687                     if (numZone == 0) {
1688                         updateChannelState(CHANNEL_FREQUENCY);
1689                     } else {
1690                         updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1691                     }
1692                     break;
1693                 case KEY_DIMMER:
1694                     brightness = Integer.parseInt(value);
1695                     updateChannelState(CHANNEL_BRIGHTNESS);
1696                     updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1697                     break;
1698                 case KEY_UPDATE_MODE:
1699                 case KEY_DISPLAY_UPDATE:
1700                     break;
1701                 case KEY_TONE:
1702                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1703                         tcbypass = false;
1704                         updateChannelState(CHANNEL_TCBYPASS);
1705                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1706                         tcbypass = true;
1707                         updateChannelState(CHANNEL_TCBYPASS);
1708                     } else {
1709                         throw new RotelException("Invalid value");
1710                     }
1711                     break;
1712                 case KEY_TCBYPASS:
1713                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1714                         tcbypass = true;
1715                         updateChannelState(CHANNEL_TCBYPASS);
1716                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1717                         tcbypass = false;
1718                         updateChannelState(CHANNEL_TCBYPASS);
1719                     } else {
1720                         throw new RotelException("Invalid value");
1721                     }
1722                     break;
1723                 case KEY_BALANCE:
1724                 case KEY_BALANCE_ZONE1:
1725                 case KEY_BALANCE_ZONE2:
1726                 case KEY_BALANCE_ZONE3:
1727                 case KEY_BALANCE_ZONE4:
1728                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1729                         balances[numZone] = minBalanceLevel;
1730                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1731                         balances[numZone] = maxBalanceLevel;
1732                     } else if (value.toUpperCase().startsWith("L")) {
1733                         balances[numZone] = -Integer.parseInt(value.substring(1));
1734                     } else if (value.toUpperCase().startsWith("R")) {
1735                         balances[numZone] = Integer.parseInt(value.substring(1));
1736                     } else {
1737                         balances[numZone] = Integer.parseInt(value);
1738                     }
1739                     if (numZone == 0) {
1740                         updateChannelState(CHANNEL_BALANCE);
1741                     } else {
1742                         updateGroupChannelState(numZone, CHANNEL_BALANCE);
1743                     }
1744                     break;
1745                 case KEY_SPEAKER:
1746                     if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1747                         speakera = true;
1748                         speakerb = false;
1749                         updateChannelState(CHANNEL_SPEAKER_A);
1750                         updateChannelState(CHANNEL_SPEAKER_B);
1751                     } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1752                         speakera = false;
1753                         speakerb = true;
1754                         updateChannelState(CHANNEL_SPEAKER_A);
1755                         updateChannelState(CHANNEL_SPEAKER_B);
1756                     } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1757                         speakera = true;
1758                         speakerb = true;
1759                         updateChannelState(CHANNEL_SPEAKER_A);
1760                         updateChannelState(CHANNEL_SPEAKER_B);
1761                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1762                         speakera = false;
1763                         speakerb = false;
1764                         updateChannelState(CHANNEL_SPEAKER_A);
1765                         updateChannelState(CHANNEL_SPEAKER_B);
1766                     } else {
1767                         throw new RotelException("Invalid value");
1768                     }
1769                     break;
1770                 case KEY_SUB_LEVEL:
1771                     logger.debug("Sub level is set to {}", value);
1772                     break;
1773                 case KEY_CENTER_LEVEL:
1774                     logger.debug("Center level is set to {}", value);
1775                     break;
1776                 case KEY_SURROUND_RIGHT_LEVEL:
1777                     logger.debug("Surround right level is set to {}", value);
1778                     break;
1779                 case KEY_SURROUND_LEFT_LEVEL:
1780                     logger.debug("Surround left level is set to {}", value);
1781                     break;
1782                 case KEY_CENTER_BACK_RIGHT_LEVEL:
1783                     logger.debug("Center back right level is set to {}", value);
1784                     break;
1785                 case KEY_CENTER_BACK_LEFT_LEVEL:
1786                     logger.debug("Center back left level is set to {}", value);
1787                     break;
1788                 case KEY_CEILING_FRONT_RIGHT_LEVEL:
1789                     logger.debug("Ceiling front right level is set to {}", value);
1790                     break;
1791                 case KEY_CEILING_FRONT_LEFT_LEVEL:
1792                     logger.debug("Ceiling front left level is set to {}", value);
1793                     break;
1794                 case KEY_CEILING_REAR_RIGHT_LEVEL:
1795                     logger.debug("Ceiling rear right level is set to {}", value);
1796                     break;
1797                 case KEY_CEILING_REAR_LEFT_LEVEL:
1798                     logger.debug("Ceiling rear left level is set to {}", value);
1799                     break;
1800                 case KEY_PCUSB_CLASS:
1801                     logger.debug("PC-USB Audio Class is set to {}", value);
1802                     break;
1803                 case KEY_PRODUCT_TYPE:
1804                 case KEY_MODEL:
1805                     getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
1806                     break;
1807                 case KEY_PRODUCT_VERSION:
1808                 case KEY_VERSION:
1809                     getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
1810                     break;
1811                 default:
1812                     logger.debug("onNewMessageEvent: unhandled key {}", key);
1813                     break;
1814             }
1815         } catch (NumberFormatException | RotelException e) {
1816             logger.debug("Invalid value {} for key {}", value, key);
1817         }
1818     }
1819
1820     /**
1821      * Handle the received information that device power (main zone) is ON
1822      */
1823     private void handlePowerOn() {
1824         Boolean prev = powers[0];
1825         powers[0] = true;
1826         updateChannelState(CHANNEL_POWER);
1827         updateChannelState(CHANNEL_MAIN_POWER);
1828         updateChannelState(CHANNEL_ALL_POWER);
1829         if ((prev == null) || !prev) {
1830             schedulePowerOnJob();
1831         }
1832     }
1833
1834     /**
1835      * Handle the received information that device power (main zone) is OFF
1836      */
1837     private void handlePowerOff() {
1838         cancelPowerOnZoneJob(0);
1839         powers[0] = false;
1840         updateChannelState(CHANNEL_POWER);
1841         updateChannelState(CHANNEL_SOURCE);
1842         updateChannelState(CHANNEL_DSP);
1843         updateChannelState(CHANNEL_VOLUME);
1844         updateChannelState(CHANNEL_MUTE);
1845         updateChannelState(CHANNEL_BASS);
1846         updateChannelState(CHANNEL_TREBLE);
1847         updateChannelState(CHANNEL_PLAY_CONTROL);
1848         updateChannelState(CHANNEL_TRACK);
1849         updateChannelState(CHANNEL_RANDOM);
1850         updateChannelState(CHANNEL_REPEAT);
1851         updateChannelState(CHANNEL_RADIO_PRESET);
1852         updateChannelState(CHANNEL_FREQUENCY);
1853         updateChannelState(CHANNEL_BRIGHTNESS);
1854         updateChannelState(CHANNEL_TCBYPASS);
1855         updateChannelState(CHANNEL_BALANCE);
1856         updateChannelState(CHANNEL_SPEAKER_A);
1857         updateChannelState(CHANNEL_SPEAKER_B);
1858
1859         updateChannelState(CHANNEL_MAIN_POWER);
1860         updateChannelState(CHANNEL_MAIN_SOURCE);
1861         updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1862         updateChannelState(CHANNEL_MAIN_DSP);
1863         updateChannelState(CHANNEL_MAIN_VOLUME);
1864         updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1865         updateChannelState(CHANNEL_MAIN_MUTE);
1866         updateChannelState(CHANNEL_MAIN_BASS);
1867         updateChannelState(CHANNEL_MAIN_TREBLE);
1868
1869         updateChannelState(CHANNEL_ALL_POWER);
1870         updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1871     }
1872
1873     /**
1874      * Handle the received information that a zone power is ON
1875      */
1876     private void handlePowerOnZone(int numZone) {
1877         Boolean prev = powers[numZone];
1878         powers[numZone] = true;
1879         updateGroupChannelState(numZone, CHANNEL_POWER);
1880         if ((prev == null) || !prev) {
1881             schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
1882         }
1883     }
1884
1885     /**
1886      * Handle the received information that a zone power is OFF
1887      */
1888     private void handlePowerOffZone(int numZone) {
1889         cancelPowerOnZoneJob(numZone);
1890         powers[numZone] = false;
1891         updateGroupChannelState(numZone, CHANNEL_POWER);
1892         updateGroupChannelState(numZone, CHANNEL_SOURCE);
1893         updateGroupChannelState(numZone, CHANNEL_VOLUME);
1894         updateGroupChannelState(numZone, CHANNEL_MUTE);
1895         updateGroupChannelState(numZone, CHANNEL_BASS);
1896         updateGroupChannelState(numZone, CHANNEL_TREBLE);
1897         updateGroupChannelState(numZone, CHANNEL_BALANCE);
1898         updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1899         updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1900     }
1901
1902     /**
1903      * Schedule the job that will consider the device as OFF if no new event is received before its running
1904      *
1905      * @param switchOffAllZones true if all zones have to be considered as OFF
1906      */
1907     private void schedulePowerOffJob(boolean switchOffAllZones) {
1908         logger.debug("Schedule power OFF job");
1909         cancelPowerOffJob();
1910         powerOffJob = scheduler.schedule(() -> {
1911             logger.debug("Power OFF job");
1912             handlePowerOff();
1913             if (switchOffAllZones) {
1914                 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1915                     handlePowerOffZone(zone);
1916                 }
1917             }
1918         }, 2000, TimeUnit.MILLISECONDS);
1919     }
1920
1921     /**
1922      * Cancel the job that will consider the device as OFF
1923      */
1924     private void cancelPowerOffJob() {
1925         ScheduledFuture<?> powerOffJob = this.powerOffJob;
1926         if (powerOffJob != null && !powerOffJob.isCancelled()) {
1927             powerOffJob.cancel(true);
1928             this.powerOffJob = null;
1929         }
1930     }
1931
1932     /**
1933      * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1934      */
1935     private void schedulePowerOnJob() {
1936         logger.debug("Schedule power ON job");
1937         cancelPowerOnZoneJob(0);
1938         powerOnZoneJobs[0] = scheduler.schedule(() -> {
1939             synchronized (sequenceLock) {
1940                 logger.debug("Power ON job");
1941                 try {
1942                     switch (protocol) {
1943                         case HEX:
1944                             if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1945                                 sendCommand(getVolumeDownCommand(0));
1946                                 Thread.sleep(100);
1947                                 sendCommand(getVolumeUpCommand(0));
1948                                 Thread.sleep(100);
1949                             }
1950                             if (model.getNumberOfZones() > 1) {
1951                                 if (currentZone != 1
1952                                         && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1953                                     selectZone(1, model.getZoneSelectCmd());
1954                                 } else if (!selectingRecord) {
1955                                     sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1956                                     Thread.sleep(100);
1957                                 }
1958                             } else {
1959                                 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1960                                 Thread.sleep(100);
1961                             }
1962                             if (model.hasToneControl()) {
1963                                 if (model == RotelModel.RSX1065) {
1964                                     // No tone control select command
1965                                     sendCommand(RotelCommand.TREBLE_DOWN);
1966                                     Thread.sleep(100);
1967                                     sendCommand(RotelCommand.TREBLE_UP);
1968                                     Thread.sleep(100);
1969                                     sendCommand(RotelCommand.BASS_DOWN);
1970                                     Thread.sleep(100);
1971                                     sendCommand(RotelCommand.BASS_UP);
1972                                     Thread.sleep(100);
1973                                 } else {
1974                                     selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1975                                 }
1976                             }
1977                             break;
1978                         case ASCII_V1:
1979                             if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1980                                     && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1981                                 sendCommand(RotelCommand.UPDATE_AUTO);
1982                                 Thread.sleep(SLEEP_INTV);
1983                             }
1984                             if (model.hasSourceControl()) {
1985                                 sendCommand(RotelCommand.SOURCE);
1986                                 Thread.sleep(SLEEP_INTV);
1987                             }
1988                             if (model.hasVolumeControl() || model.hasToneControl()) {
1989                                 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1990                                         && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1991                                     sendCommand(RotelCommand.VOLUME_GET_MIN);
1992                                     Thread.sleep(SLEEP_INTV);
1993                                     sendCommand(RotelCommand.VOLUME_GET_MAX);
1994                                     Thread.sleep(SLEEP_INTV);
1995                                 }
1996                                 if (model.hasToneControl()) {
1997                                     sendCommand(RotelCommand.TONE_MAX);
1998                                     Thread.sleep(SLEEP_INTV);
1999                                 }
2000                                 // Wait enough to be sure to get the min/max values requested just before
2001                                 Thread.sleep(250);
2002                                 if (model.hasVolumeControl()) {
2003                                     sendCommand(RotelCommand.VOLUME_GET);
2004                                     Thread.sleep(SLEEP_INTV);
2005                                     if (model != RotelModel.RA11 && model != RotelModel.RA12
2006                                             && model != RotelModel.RCX1500) {
2007                                         sendCommand(RotelCommand.MUTE);
2008                                         Thread.sleep(SLEEP_INTV);
2009                                     }
2010                                 }
2011                                 if (model.hasToneControl()) {
2012                                     sendCommand(RotelCommand.BASS);
2013                                     Thread.sleep(SLEEP_INTV);
2014                                     sendCommand(RotelCommand.TREBLE);
2015                                     Thread.sleep(SLEEP_INTV);
2016                                     if (model.canGetBypassStatus()) {
2017                                         sendCommand(RotelCommand.TONE_CONTROLS);
2018                                         Thread.sleep(SLEEP_INTV);
2019                                     }
2020                                 }
2021                             }
2022                             if (model.hasBalanceControl()) {
2023                                 sendCommand(RotelCommand.BALANCE);
2024                                 Thread.sleep(SLEEP_INTV);
2025                             }
2026                             if (model.hasPlayControl()) {
2027                                 RotelSource source = sources[0];
2028                                 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
2029                                         && (model != RotelModel.RCX1500 || source == null
2030                                                 || !source.getName().equals("CD"))) {
2031                                     sendCommand(RotelCommand.PLAY_STATUS);
2032                                     Thread.sleep(SLEEP_INTV);
2033                                 } else {
2034                                     sendCommand(RotelCommand.CD_PLAY_STATUS);
2035                                     Thread.sleep(SLEEP_INTV);
2036                                 }
2037                             }
2038                             if (model.hasDspControl()) {
2039                                 sendCommand(RotelCommand.DSP_MODE);
2040                                 Thread.sleep(SLEEP_INTV);
2041                             }
2042                             if (model.canGetFrequency()) {
2043                                 sendCommand(RotelCommand.FREQUENCY);
2044                                 Thread.sleep(SLEEP_INTV);
2045                             }
2046                             if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
2047                                 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
2048                                 Thread.sleep(SLEEP_INTV);
2049                             }
2050                             if (model.hasSpeakerGroups()) {
2051                                 sendCommand(RotelCommand.SPEAKER);
2052                                 Thread.sleep(SLEEP_INTV);
2053                             }
2054                             if (model != RotelModel.RAP1580 && model != RotelModel.RSP1576
2055                                     && model != RotelModel.RSP1582) {
2056                                 sendCommand(RotelCommand.MODEL);
2057                                 Thread.sleep(SLEEP_INTV);
2058                                 sendCommand(RotelCommand.VERSION);
2059                                 Thread.sleep(SLEEP_INTV);
2060                             }
2061                             break;
2062                         case ASCII_V2:
2063                             sendCommand(RotelCommand.UPDATE_AUTO);
2064                             Thread.sleep(SLEEP_INTV);
2065                             if (model.hasSourceControl()) {
2066                                 if (model.getNumberOfZones() > 1) {
2067                                     sendCommand(RotelCommand.INPUT);
2068                                 } else {
2069                                     sendCommand(RotelCommand.SOURCE);
2070                                 }
2071                                 Thread.sleep(SLEEP_INTV);
2072                             }
2073                             if (model.hasVolumeControl()) {
2074                                 sendCommand(RotelCommand.VOLUME_GET);
2075                                 Thread.sleep(SLEEP_INTV);
2076                                 sendCommand(RotelCommand.MUTE);
2077                                 Thread.sleep(SLEEP_INTV);
2078                             }
2079                             if (model.hasToneControl()) {
2080                                 sendCommand(RotelCommand.BASS);
2081                                 Thread.sleep(SLEEP_INTV);
2082                                 sendCommand(RotelCommand.TREBLE);
2083                                 Thread.sleep(SLEEP_INTV);
2084                                 if (model.canGetBypassStatus()) {
2085                                     sendCommand(RotelCommand.TCBYPASS);
2086                                     Thread.sleep(SLEEP_INTV);
2087                                 }
2088                             }
2089                             if (model.hasBalanceControl()) {
2090                                 sendCommand(RotelCommand.BALANCE);
2091                                 Thread.sleep(SLEEP_INTV);
2092                             }
2093                             if (model.hasPlayControl()) {
2094                                 sendCommand(RotelCommand.PLAY_STATUS);
2095                                 Thread.sleep(SLEEP_INTV);
2096                                 RotelSource source = sources[0];
2097                                 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
2098                                     sendCommand(RotelCommand.TRACK);
2099                                     Thread.sleep(SLEEP_INTV);
2100                                     sendCommand(RotelCommand.RANDOM_MODE);
2101                                     Thread.sleep(SLEEP_INTV);
2102                                     sendCommand(RotelCommand.REPEAT_MODE);
2103                                     Thread.sleep(SLEEP_INTV);
2104                                 }
2105                             }
2106                             if (model.hasDspControl()) {
2107                                 sendCommand(RotelCommand.DSP_MODE);
2108                                 Thread.sleep(SLEEP_INTV);
2109                             }
2110                             if (model.canGetFrequency()) {
2111                                 sendCommand(RotelCommand.FREQUENCY);
2112                                 Thread.sleep(SLEEP_INTV);
2113                             }
2114                             if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
2115                                 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
2116                                 Thread.sleep(SLEEP_INTV);
2117                             }
2118                             if (model.hasSpeakerGroups()) {
2119                                 sendCommand(RotelCommand.SPEAKER);
2120                                 Thread.sleep(SLEEP_INTV);
2121                             }
2122                             sendCommand(RotelCommand.MODEL);
2123                             Thread.sleep(SLEEP_INTV);
2124                             sendCommand(RotelCommand.VERSION);
2125                             Thread.sleep(SLEEP_INTV);
2126                             break;
2127                     }
2128                 } catch (RotelException e) {
2129                     logger.debug("Init sequence failed: {}", e.getMessage());
2130                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2131                             "@text/offline.comm-error-init-sequence");
2132                     closeConnection();
2133                 } catch (InterruptedException e) {
2134                     logger.debug("Init sequence interrupted: {}", e.getMessage());
2135                     Thread.currentThread().interrupt();
2136                 }
2137             }
2138         }, 2500, TimeUnit.MILLISECONDS);
2139     }
2140
2141     /**
2142      * Schedule the job to run with a few seconds delay when the zone power switched ON
2143      */
2144     private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
2145         logger.debug("Schedule power ON zone {} job", numZone);
2146         cancelPowerOnZoneJob(numZone);
2147         powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
2148             synchronized (sequenceLock) {
2149                 logger.debug("Power ON zone {} job", numZone);
2150                 try {
2151                     if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
2152                         selectZone(numZone, model.getZoneSelectCmd());
2153                         sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
2154                         Thread.sleep(100);
2155                         sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
2156                         Thread.sleep(100);
2157                     }
2158                 } catch (RotelException e) {
2159                     logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
2160                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2161                             String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
2162                     closeConnection();
2163                 } catch (InterruptedException e) {
2164                     logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
2165                     Thread.currentThread().interrupt();
2166                 }
2167             }
2168         }, 2500, TimeUnit.MILLISECONDS);
2169     }
2170
2171     /**
2172      * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
2173      */
2174     private void cancelPowerOnZoneJob(int numZone) {
2175         ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
2176         if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
2177             powerOnZoneJob.cancel(true);
2178             powerOnZoneJobs[numZone] = null;
2179         }
2180     }
2181
2182     /**
2183      * Schedule the reconnection job
2184      */
2185     private void scheduleReconnectJob() {
2186         logger.debug("Schedule reconnect job");
2187         cancelReconnectJob();
2188         reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2189             if (!connector.isConnected()) {
2190                 logger.debug("Trying to reconnect...");
2191                 closeConnection();
2192                 powers[0] = null;
2193                 String error = null;
2194                 if (openConnection()) {
2195                     synchronized (sequenceLock) {
2196                         schedulePowerOffJob(true);
2197                         try {
2198                             sendCommand(model.getPowerStateCmd());
2199                         } catch (RotelException e) {
2200                             error = "@text/offline.comm-error-first-command-after-reconnection";
2201                             logger.debug("First command after connection failed", e);
2202                             cancelPowerOffJob();
2203                             closeConnection();
2204                         }
2205                     }
2206                 } else {
2207                     error = "@text/offline.comm-error-reconnection";
2208                 }
2209                 if (error != null) {
2210                     handlePowerOff();
2211                     for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
2212                         handlePowerOffZone(zone);
2213                     }
2214                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2215                 } else {
2216                     updateStatus(ThingStatus.ONLINE);
2217                 }
2218             }
2219         }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2220     }
2221
2222     /**
2223      * Cancel the reconnection job
2224      */
2225     private void cancelReconnectJob() {
2226         ScheduledFuture<?> reconnectJob = this.reconnectJob;
2227         if (reconnectJob != null && !reconnectJob.isCancelled()) {
2228             reconnectJob.cancel(true);
2229             this.reconnectJob = null;
2230         }
2231     }
2232
2233     private void updateGroupChannelState(int numZone, String channel) {
2234         updateChannelState(String.format("zone%d#%s", numZone, channel));
2235     }
2236
2237     /**
2238      * Update the state of a channel
2239      *
2240      * @param channel the channel
2241      */
2242     private void updateChannelState(String channel) {
2243         if (!isLinked(channel)) {
2244             return;
2245         }
2246         State state = UnDefType.UNDEF;
2247         RotelSource localSource;
2248         int numZone = 0;
2249         switch (channel) {
2250             case CHANNEL_ZONE1_SOURCE:
2251             case CHANNEL_ZONE1_VOLUME:
2252             case CHANNEL_ZONE1_MUTE:
2253             case CHANNEL_ZONE1_BASS:
2254             case CHANNEL_ZONE1_TREBLE:
2255             case CHANNEL_ZONE1_BALANCE:
2256             case CHANNEL_ZONE1_FREQUENCY:
2257                 numZone = 1;
2258                 break;
2259             case CHANNEL_ZONE2_POWER:
2260             case CHANNEL_ZONE2_SOURCE:
2261             case CHANNEL_ZONE2_VOLUME:
2262             case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2263             case CHANNEL_ZONE2_MUTE:
2264             case CHANNEL_ZONE2_BASS:
2265             case CHANNEL_ZONE2_TREBLE:
2266             case CHANNEL_ZONE2_BALANCE:
2267             case CHANNEL_ZONE2_FREQUENCY:
2268                 numZone = 2;
2269                 break;
2270             case CHANNEL_ZONE3_POWER:
2271             case CHANNEL_ZONE3_SOURCE:
2272             case CHANNEL_ZONE3_VOLUME:
2273             case CHANNEL_ZONE3_MUTE:
2274             case CHANNEL_ZONE3_BASS:
2275             case CHANNEL_ZONE3_TREBLE:
2276             case CHANNEL_ZONE3_BALANCE:
2277             case CHANNEL_ZONE3_FREQUENCY:
2278                 numZone = 3;
2279                 break;
2280             case CHANNEL_ZONE4_POWER:
2281             case CHANNEL_ZONE4_SOURCE:
2282             case CHANNEL_ZONE4_VOLUME:
2283             case CHANNEL_ZONE4_MUTE:
2284             case CHANNEL_ZONE4_BASS:
2285             case CHANNEL_ZONE4_TREBLE:
2286             case CHANNEL_ZONE4_BALANCE:
2287             case CHANNEL_ZONE4_FREQUENCY:
2288                 numZone = 4;
2289                 break;
2290             default:
2291                 break;
2292         }
2293         switch (channel) {
2294             case CHANNEL_POWER:
2295             case CHANNEL_MAIN_POWER:
2296             case CHANNEL_ALL_POWER:
2297             case CHANNEL_ZONE2_POWER:
2298             case CHANNEL_ZONE3_POWER:
2299             case CHANNEL_ZONE4_POWER:
2300                 Boolean powerZone = powers[numZone];
2301                 if (powerZone != null) {
2302                     state = OnOffType.from(powerZone.booleanValue());
2303                 }
2304                 break;
2305             case CHANNEL_SOURCE:
2306             case CHANNEL_MAIN_SOURCE:
2307             case CHANNEL_ZONE1_SOURCE:
2308             case CHANNEL_ZONE2_SOURCE:
2309             case CHANNEL_ZONE3_SOURCE:
2310             case CHANNEL_ZONE4_SOURCE:
2311                 localSource = sources[numZone];
2312                 if (isPowerOn(numZone) && localSource != null) {
2313                     state = new StringType(localSource.getName());
2314                 }
2315                 break;
2316             case CHANNEL_MAIN_RECORD_SOURCE:
2317                 localSource = recordSource;
2318                 if (isPowerOn() && localSource != null) {
2319                     state = new StringType(localSource.getName());
2320                 }
2321                 break;
2322             case CHANNEL_DSP:
2323             case CHANNEL_MAIN_DSP:
2324                 if (isPowerOn()) {
2325                     state = new StringType(dsp.getName());
2326                 }
2327                 break;
2328             case CHANNEL_VOLUME:
2329             case CHANNEL_MAIN_VOLUME:
2330             case CHANNEL_ZONE1_VOLUME:
2331             case CHANNEL_ZONE2_VOLUME:
2332             case CHANNEL_ZONE3_VOLUME:
2333             case CHANNEL_ZONE4_VOLUME:
2334                 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2335                     long volumePct = Math
2336                             .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2337                     state = new PercentType(BigDecimal.valueOf(volumePct));
2338                 }
2339                 break;
2340             case CHANNEL_MAIN_VOLUME_UP_DOWN:
2341             case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2342                 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2343                     state = new DecimalType(volumes[numZone]);
2344                 }
2345                 break;
2346             case CHANNEL_MUTE:
2347             case CHANNEL_MAIN_MUTE:
2348             case CHANNEL_ZONE1_MUTE:
2349             case CHANNEL_ZONE2_MUTE:
2350             case CHANNEL_ZONE3_MUTE:
2351             case CHANNEL_ZONE4_MUTE:
2352                 if (isPowerOn(numZone)) {
2353                     state = OnOffType.from(mutes[numZone]);
2354                 }
2355                 break;
2356             case CHANNEL_BASS:
2357             case CHANNEL_MAIN_BASS:
2358             case CHANNEL_ZONE1_BASS:
2359             case CHANNEL_ZONE2_BASS:
2360             case CHANNEL_ZONE3_BASS:
2361             case CHANNEL_ZONE4_BASS:
2362                 if (isPowerOn(numZone)) {
2363                     state = new DecimalType(basses[numZone]);
2364                 }
2365                 break;
2366             case CHANNEL_TREBLE:
2367             case CHANNEL_MAIN_TREBLE:
2368             case CHANNEL_ZONE1_TREBLE:
2369             case CHANNEL_ZONE2_TREBLE:
2370             case CHANNEL_ZONE3_TREBLE:
2371             case CHANNEL_ZONE4_TREBLE:
2372                 if (isPowerOn(numZone)) {
2373                     state = new DecimalType(trebles[numZone]);
2374                 }
2375                 break;
2376             case CHANNEL_TRACK:
2377                 if (isPowerOn() && track > 0) {
2378                     state = new DecimalType(track);
2379                 }
2380                 break;
2381             case CHANNEL_RANDOM:
2382                 if (isPowerOn()) {
2383                     state = OnOffType.from(randomMode);
2384                 }
2385                 break;
2386             case CHANNEL_REPEAT:
2387                 if (isPowerOn()) {
2388                     state = new StringType(repeatMode.name());
2389                 }
2390                 break;
2391             case CHANNEL_PLAY_CONTROL:
2392                 if (isPowerOn()) {
2393                     switch (playStatus) {
2394                         case PLAYING:
2395                             state = PlayPauseType.PLAY;
2396                             break;
2397                         case PAUSED:
2398                         case STOPPED:
2399                             state = PlayPauseType.PAUSE;
2400                             break;
2401                     }
2402                 }
2403                 break;
2404             case CHANNEL_RADIO_PRESET:
2405                 if (isPowerOn()) {
2406                     state = radioPreset == 0 ? UnDefType.UNDEF : new DecimalType(radioPreset);
2407                 }
2408                 break;
2409             case CHANNEL_FREQUENCY:
2410             case CHANNEL_ZONE1_FREQUENCY:
2411             case CHANNEL_ZONE2_FREQUENCY:
2412             case CHANNEL_ZONE3_FREQUENCY:
2413             case CHANNEL_ZONE4_FREQUENCY:
2414                 if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
2415                     state = new DecimalType(frequencies[numZone]);
2416                 }
2417                 break;
2418             case CHANNEL_LINE1:
2419                 state = new StringType(frontPanelLine1);
2420                 break;
2421             case CHANNEL_LINE2:
2422                 state = new StringType(frontPanelLine2);
2423                 break;
2424             case CHANNEL_BRIGHTNESS:
2425             case CHANNEL_ALL_BRIGHTNESS:
2426                 if (isPowerOn() && model.hasDimmerControl()) {
2427                     long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2428                             / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2429                     state = new PercentType(BigDecimal.valueOf(dimmerPct));
2430                 }
2431                 break;
2432             case CHANNEL_TCBYPASS:
2433                 if (isPowerOn()) {
2434                     state = OnOffType.from(tcbypass);
2435                 }
2436                 break;
2437             case CHANNEL_BALANCE:
2438             case CHANNEL_ZONE1_BALANCE:
2439             case CHANNEL_ZONE2_BALANCE:
2440             case CHANNEL_ZONE3_BALANCE:
2441             case CHANNEL_ZONE4_BALANCE:
2442                 if (isPowerOn(numZone)) {
2443                     state = new DecimalType(balances[numZone]);
2444                 }
2445                 break;
2446             case CHANNEL_SPEAKER_A:
2447                 if (isPowerOn()) {
2448                     state = OnOffType.from(speakera);
2449                 }
2450                 break;
2451             case CHANNEL_SPEAKER_B:
2452                 if (isPowerOn()) {
2453                     state = OnOffType.from(speakerb);
2454                 }
2455                 break;
2456             default:
2457                 break;
2458         }
2459         updateState(channel, state);
2460     }
2461
2462     /**
2463      * Inform about the device / main zone power state
2464      *
2465      * @return true if device / main zone power state is known and known as ON
2466      */
2467     private boolean isPowerOn() {
2468         return isPowerOn(0);
2469     }
2470
2471     /**
2472      * Inform about the power state
2473      *
2474      * @param numZone the zone number (1-4) or 0 for the device or main zone
2475      *
2476      * @return true if power state is known and known as ON
2477      */
2478     private boolean isPowerOn(int numZone) {
2479         if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
2480             throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
2481         }
2482         Boolean power = powers[numZone];
2483         return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
2484     }
2485
2486     /**
2487      * Get the command to be used for POWER ON
2488      *
2489      * @param numZone the zone number (2-4) or 0 for the device or main zone
2490      *
2491      * @return the command
2492      */
2493     private RotelCommand getPowerOnCommand(int numZone) {
2494         switch (numZone) {
2495             case 0:
2496                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2497             case 2:
2498                 return RotelCommand.ZONE2_POWER_ON;
2499             case 3:
2500                 return RotelCommand.ZONE3_POWER_ON;
2501             case 4:
2502                 return RotelCommand.ZONE4_POWER_ON;
2503             default:
2504                 throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
2505         }
2506     }
2507
2508     /**
2509      * Get the command to be used for POWER OFF
2510      *
2511      * @param numZone the zone number (2-4) or 0 for the device or main zone
2512      *
2513      * @return the command
2514      */
2515     private RotelCommand getPowerOffCommand(int numZone) {
2516         switch (numZone) {
2517             case 0:
2518                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2519             case 2:
2520                 return RotelCommand.ZONE2_POWER_OFF;
2521             case 3:
2522                 return RotelCommand.ZONE3_POWER_OFF;
2523             case 4:
2524                 return RotelCommand.ZONE4_POWER_OFF;
2525             default:
2526                 throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
2527         }
2528     }
2529
2530     /**
2531      * Get the command to be used for VOLUME UP
2532      *
2533      * @param numZone the zone number (1-4) or 0 for the device or main zone
2534      *
2535      * @return the command
2536      */
2537     private RotelCommand getVolumeUpCommand(int numZone) {
2538         switch (numZone) {
2539             case 0:
2540                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2541             case 1:
2542                 return RotelCommand.ZONE1_VOLUME_UP;
2543             case 2:
2544                 return RotelCommand.ZONE2_VOLUME_UP;
2545             case 3:
2546                 return RotelCommand.ZONE3_VOLUME_UP;
2547             case 4:
2548                 return RotelCommand.ZONE4_VOLUME_UP;
2549             default:
2550                 throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
2551         }
2552     }
2553
2554     /**
2555      * Get the command to be used for VOLUME DOWN
2556      *
2557      * @param numZone the zone number (1-4) or 0 for the device or main zone
2558      *
2559      * @return the command
2560      */
2561     private RotelCommand getVolumeDownCommand(int numZone) {
2562         switch (numZone) {
2563             case 0:
2564                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2565                         : RotelCommand.VOLUME_DOWN;
2566             case 1:
2567                 return RotelCommand.ZONE1_VOLUME_DOWN;
2568             case 2:
2569                 return RotelCommand.ZONE2_VOLUME_DOWN;
2570             case 3:
2571                 return RotelCommand.ZONE3_VOLUME_DOWN;
2572             case 4:
2573                 return RotelCommand.ZONE4_VOLUME_DOWN;
2574             default:
2575                 throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
2576         }
2577     }
2578
2579     /**
2580      * Get the command to be used for VOLUME SET
2581      *
2582      * @param numZone the zone number (1-4) or 0 for the device
2583      *
2584      * @return the command
2585      */
2586     private RotelCommand getVolumeSetCommand(int numZone) {
2587         switch (numZone) {
2588             case 0:
2589                 return RotelCommand.VOLUME_SET;
2590             case 1:
2591                 return RotelCommand.ZONE1_VOLUME_SET;
2592             case 2:
2593                 return RotelCommand.ZONE2_VOLUME_SET;
2594             case 3:
2595                 return RotelCommand.ZONE3_VOLUME_SET;
2596             case 4:
2597                 return RotelCommand.ZONE4_VOLUME_SET;
2598             default:
2599                 throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
2600         }
2601     }
2602
2603     /**
2604      * Get the command to be used for MUTE ON
2605      *
2606      * @param numZone the zone number (1-4) or 0 for the device or main zone
2607      *
2608      * @return the command
2609      */
2610     private RotelCommand getMuteOnCommand(int numZone) {
2611         switch (numZone) {
2612             case 0:
2613                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2614             case 1:
2615                 return RotelCommand.ZONE1_MUTE_ON;
2616             case 2:
2617                 return RotelCommand.ZONE2_MUTE_ON;
2618             case 3:
2619                 return RotelCommand.ZONE3_MUTE_ON;
2620             case 4:
2621                 return RotelCommand.ZONE4_MUTE_ON;
2622             default:
2623                 throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
2624         }
2625     }
2626
2627     /**
2628      * Get the command to be used for MUTE OFF
2629      *
2630      * @param numZone the zone number (1-4) or 0 for the device or main zone
2631      *
2632      * @return the command
2633      */
2634     private RotelCommand getMuteOffCommand(int numZone) {
2635         switch (numZone) {
2636             case 0:
2637                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2638             case 1:
2639                 return RotelCommand.ZONE1_MUTE_OFF;
2640             case 2:
2641                 return RotelCommand.ZONE2_MUTE_OFF;
2642             case 3:
2643                 return RotelCommand.ZONE3_MUTE_OFF;
2644             case 4:
2645                 return RotelCommand.ZONE4_MUTE_OFF;
2646             default:
2647                 throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
2648         }
2649     }
2650
2651     /**
2652      * Get the command to be used for MUTE TOGGLE
2653      *
2654      * @param numZone the zone number (1-4) or 0 for the device or main zone
2655      *
2656      * @return the command
2657      */
2658     private RotelCommand getMuteToggleCommand(int numZone) {
2659         switch (numZone) {
2660             case 0:
2661                 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2662                         : RotelCommand.MUTE_TOGGLE;
2663             case 1:
2664                 return RotelCommand.ZONE1_MUTE_TOGGLE;
2665             case 2:
2666                 return RotelCommand.ZONE2_MUTE_TOGGLE;
2667             case 3:
2668                 return RotelCommand.ZONE3_MUTE_TOGGLE;
2669             case 4:
2670                 return RotelCommand.ZONE4_MUTE_TOGGLE;
2671             default:
2672                 throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
2673         }
2674     }
2675
2676     /**
2677      * Get the command to be used for BASS UP
2678      *
2679      * @param numZone the zone number (1-4) or 0 for the device
2680      *
2681      * @return the command
2682      */
2683     private RotelCommand getBassUpCommand(int numZone) {
2684         switch (numZone) {
2685             case 0:
2686                 return RotelCommand.BASS_UP;
2687             case 1:
2688                 return RotelCommand.ZONE1_BASS_UP;
2689             case 2:
2690                 return RotelCommand.ZONE2_BASS_UP;
2691             case 3:
2692                 return RotelCommand.ZONE3_BASS_UP;
2693             case 4:
2694                 return RotelCommand.ZONE4_BASS_UP;
2695             default:
2696                 throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
2697         }
2698     }
2699
2700     /**
2701      * Get the command to be used for BASS DOWN
2702      *
2703      * @param numZone the zone number (1-4) or 0 for the device
2704      *
2705      * @return the command
2706      */
2707     private RotelCommand getBassDownCommand(int numZone) {
2708         switch (numZone) {
2709             case 0:
2710                 return RotelCommand.BASS_DOWN;
2711             case 1:
2712                 return RotelCommand.ZONE1_BASS_DOWN;
2713             case 2:
2714                 return RotelCommand.ZONE2_BASS_DOWN;
2715             case 3:
2716                 return RotelCommand.ZONE3_BASS_DOWN;
2717             case 4:
2718                 return RotelCommand.ZONE4_BASS_DOWN;
2719             default:
2720                 throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
2721         }
2722     }
2723
2724     /**
2725      * Get the command to be used for BASS SET
2726      *
2727      * @param numZone the zone number (1-4) or 0 for the device
2728      *
2729      * @return the command
2730      */
2731     private RotelCommand getBassSetCommand(int numZone) {
2732         switch (numZone) {
2733             case 0:
2734                 return RotelCommand.BASS_SET;
2735             case 1:
2736                 return RotelCommand.ZONE1_BASS_SET;
2737             case 2:
2738                 return RotelCommand.ZONE2_BASS_SET;
2739             case 3:
2740                 return RotelCommand.ZONE3_BASS_SET;
2741             case 4:
2742                 return RotelCommand.ZONE4_BASS_SET;
2743             default:
2744                 throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
2745         }
2746     }
2747
2748     /**
2749      * Get the command to be used for TREBLE UP
2750      *
2751      * @param numZone the zone number (1-4) or 0 for the device
2752      *
2753      * @return the command
2754      */
2755     private RotelCommand getTrebleUpCommand(int numZone) {
2756         switch (numZone) {
2757             case 0:
2758                 return RotelCommand.TREBLE_UP;
2759             case 1:
2760                 return RotelCommand.ZONE1_TREBLE_UP;
2761             case 2:
2762                 return RotelCommand.ZONE2_TREBLE_UP;
2763             case 3:
2764                 return RotelCommand.ZONE3_TREBLE_UP;
2765             case 4:
2766                 return RotelCommand.ZONE4_TREBLE_UP;
2767             default:
2768                 throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
2769         }
2770     }
2771
2772     /**
2773      * Get the command to be used for TREBLE DOWN
2774      *
2775      * @param numZone the zone number (1-4) or 0 for the device
2776      *
2777      * @return the command
2778      */
2779     private RotelCommand getTrebleDownCommand(int numZone) {
2780         switch (numZone) {
2781             case 0:
2782                 return RotelCommand.TREBLE_DOWN;
2783             case 1:
2784                 return RotelCommand.ZONE1_TREBLE_DOWN;
2785             case 2:
2786                 return RotelCommand.ZONE2_TREBLE_DOWN;
2787             case 3:
2788                 return RotelCommand.ZONE3_TREBLE_DOWN;
2789             case 4:
2790                 return RotelCommand.ZONE4_TREBLE_DOWN;
2791             default:
2792                 throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
2793         }
2794     }
2795
2796     /**
2797      * Get the command to be used for TREBLE SET
2798      *
2799      * @param numZone the zone number (1-4) or 0 for the device
2800      *
2801      * @return the command
2802      */
2803     private RotelCommand getTrebleSetCommand(int numZone) {
2804         switch (numZone) {
2805             case 0:
2806                 return RotelCommand.TREBLE_SET;
2807             case 1:
2808                 return RotelCommand.ZONE1_TREBLE_SET;
2809             case 2:
2810                 return RotelCommand.ZONE2_TREBLE_SET;
2811             case 3:
2812                 return RotelCommand.ZONE3_TREBLE_SET;
2813             case 4:
2814                 return RotelCommand.ZONE4_TREBLE_SET;
2815             default:
2816                 throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
2817         }
2818     }
2819
2820     /**
2821      * Get the command to be used for BALANCE LEFT
2822      *
2823      * @param numZone the zone number (1-4) or 0 for the device
2824      *
2825      * @return the command
2826      */
2827     private RotelCommand getBalanceLeftCommand(int numZone) {
2828         switch (numZone) {
2829             case 0:
2830                 return RotelCommand.BALANCE_LEFT;
2831             case 1:
2832                 return RotelCommand.ZONE1_BALANCE_LEFT;
2833             case 2:
2834                 return RotelCommand.ZONE2_BALANCE_LEFT;
2835             case 3:
2836                 return RotelCommand.ZONE3_BALANCE_LEFT;
2837             case 4:
2838                 return RotelCommand.ZONE4_BALANCE_LEFT;
2839             default:
2840                 throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
2841         }
2842     }
2843
2844     /**
2845      * Get the command to be used for BALANCE RIGHT
2846      *
2847      * @param numZone the zone number (1-4) or 0 for the device
2848      *
2849      * @return the command
2850      */
2851     private RotelCommand getBalanceRightCommand(int numZone) {
2852         switch (numZone) {
2853             case 0:
2854                 return RotelCommand.BALANCE_RIGHT;
2855             case 1:
2856                 return RotelCommand.ZONE1_BALANCE_RIGHT;
2857             case 2:
2858                 return RotelCommand.ZONE2_BALANCE_RIGHT;
2859             case 3:
2860                 return RotelCommand.ZONE3_BALANCE_RIGHT;
2861             case 4:
2862                 return RotelCommand.ZONE4_BALANCE_RIGHT;
2863             default:
2864                 throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
2865         }
2866     }
2867
2868     /**
2869      * Get the command to be used for BALANCE SET
2870      *
2871      * @param numZone the zone number (1-4) or 0 for the device
2872      *
2873      * @return the command
2874      */
2875     private RotelCommand getBalanceSetCommand(int numZone) {
2876         switch (numZone) {
2877             case 0:
2878                 return RotelCommand.BALANCE_SET;
2879             case 1:
2880                 return RotelCommand.ZONE1_BALANCE_SET;
2881             case 2:
2882                 return RotelCommand.ZONE2_BALANCE_SET;
2883             case 3:
2884                 return RotelCommand.ZONE3_BALANCE_SET;
2885             case 4:
2886                 return RotelCommand.ZONE4_BALANCE_SET;
2887             default:
2888                 throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
2889         }
2890     }
2891
2892     private @Nullable RotelCommand getRadioPresetGetCommand(RotelSource source) {
2893         if (protocol == RotelProtocol.ASCII_V1) {
2894             switch (source.getName()) {
2895                 case "FM":
2896                 case "DAB":
2897                 case "IRADIO":
2898                     return RotelCommand.PRESET;
2899                 default:
2900                     break;
2901             }
2902         } else if (protocol == RotelProtocol.ASCII_V2) {
2903             switch (source.getName()) {
2904                 case "FM":
2905                     return RotelCommand.FM_PRESET;
2906                 case "DAB":
2907                     return RotelCommand.DAB_PRESET;
2908                 default:
2909                     break;
2910             }
2911         }
2912         return null;
2913     }
2914
2915     private @Nullable RotelCommand getRadioPresetCallCommand(RotelSource source) {
2916         switch (source.getName()) {
2917             case "FM":
2918                 return RotelCommand.CALL_FM_PRESET;
2919             case "DAB":
2920                 return RotelCommand.CALL_DAB_PRESET;
2921             case "IRADIO":
2922                 return RotelCommand.CALL_IRADIO_PRESET;
2923             default:
2924                 break;
2925         }
2926         return null;
2927     }
2928
2929     private void sendCommand(RotelCommand cmd) throws RotelException {
2930         sendCommand(cmd, null);
2931     }
2932
2933     /**
2934      * Request the Rotel device to execute a command
2935      *
2936      * @param cmd the command to execute
2937      * @param value the integer value to consider for volume, bass or treble adjustment
2938      *
2939      * @throws RotelException - In case of any problem
2940      */
2941     private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2942         byte[] message;
2943         try {
2944             message = protocolHandler.buildCommandMessage(cmd, value);
2945         } catch (RotelException e) {
2946             // Command not supported
2947             logger.debug("sendCommand: {}", e.getMessage());
2948             return;
2949         }
2950         connector.writeOutput(cmd, message);
2951
2952         if (connector instanceof RotelSimuConnector) {
2953             if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2954                     || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2955                     || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2956                 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);
2957             }
2958         }
2959     }
2960 }