]> git.basschouten.com Git - openhab-addons.git/blob
11dca6205962bd203f7e9be41b03a237b86cc21f
[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.RotelException;
30 import org.openhab.binding.rotel.internal.RotelModel;
31 import org.openhab.binding.rotel.internal.RotelPlayStatus;
32 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
33 import org.openhab.binding.rotel.internal.communication.RotelCommand;
34 import org.openhab.binding.rotel.internal.communication.RotelConnector;
35 import org.openhab.binding.rotel.internal.communication.RotelDsp;
36 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
38 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSource;
40 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
41 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
42 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
43 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
44 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
45 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
46 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
47 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
48 import org.openhab.core.io.transport.serial.SerialPortManager;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.IncreaseDecreaseType;
51 import org.openhab.core.library.types.NextPreviousType;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.PercentType;
54 import org.openhab.core.library.types.PlayPauseType;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.StateOption;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
71  *
72  * @author Laurent Garnier - Initial contribution
73  */
74 @NonNullByDefault
75 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
76
77     private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
78
79     private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
80     private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
81     private static final boolean USE_SIMULATED_DEVICE = false;
82     private static final int SLEEP_INTV = 30;
83
84     private @Nullable ScheduledFuture<?> reconnectJob;
85     private @Nullable ScheduledFuture<?> powerOnJob;
86     private @Nullable ScheduledFuture<?> powerOffJob;
87     private @Nullable ScheduledFuture<?> powerOnZone2Job;
88     private @Nullable ScheduledFuture<?> powerOnZone3Job;
89     private @Nullable ScheduledFuture<?> powerOnZone4Job;
90
91     private RotelStateDescriptionOptionProvider stateDescriptionProvider;
92     private SerialPortManager serialPortManager;
93
94     private RotelModel model;
95     private RotelProtocol protocol;
96     private RotelAbstractProtocolHandler protocolHandler;
97     private RotelConnector connector;
98
99     private int minVolume;
100     private int maxVolume;
101     private int minToneLevel;
102     private int maxToneLevel;
103
104     private int currentZone = 1;
105     private boolean selectingRecord;
106     private @Nullable Boolean power;
107     private boolean powerZone2;
108     private boolean powerZone3;
109     private boolean powerZone4;
110     private RotelSource source = RotelSource.CAT0_CD;
111     private @Nullable RotelSource recordSource;
112     private @Nullable RotelSource sourceZone2;
113     private @Nullable RotelSource sourceZone3;
114     private @Nullable RotelSource sourceZone4;
115     private RotelDsp dsp = RotelDsp.CAT1_NONE;
116     private int volume;
117     private boolean mute;
118     private boolean fixedVolumeZone2;
119     private int volumeZone2;
120     private boolean muteZone2;
121     private boolean fixedVolumeZone3;
122     private int volumeZone3;
123     private boolean muteZone3;
124     private boolean fixedVolumeZone4;
125     private int volumeZone4;
126     private boolean muteZone4;
127     private int bass;
128     private int treble;
129     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
130     private int track;
131     private double frequency;
132     private String frontPanelLine1 = "";
133     private String frontPanelLine2 = "";
134     private int brightness;
135     private boolean tcbypass;
136     private int balance;
137     private int minBalanceLevel;
138     private int maxBalanceLevel;
139     private boolean speakera;
140     private boolean speakerb;
141
142     private Object sequenceLock = new Object();
143
144     /**
145      * Constructor
146      */
147     public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
148             SerialPortManager serialPortManager) {
149         super(thing);
150         this.stateDescriptionProvider = stateDescriptionProvider;
151         this.serialPortManager = serialPortManager;
152         this.model = DEFAULT_MODEL;
153         this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
154         this.protocol = protocolHandler.getProtocol();
155         this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
156     }
157
158     @Override
159     public void initialize() {
160         logger.debug("Start initializing handler for thing {}", getThing().getUID());
161
162         switch (getThing().getThingTypeUID().getId()) {
163             case THING_TYPE_ID_RSP1066:
164                 model = RotelModel.RSP1066;
165                 break;
166             case THING_TYPE_ID_RSP1068:
167                 model = RotelModel.RSP1068;
168                 break;
169             case THING_TYPE_ID_RSP1069:
170                 model = RotelModel.RSP1069;
171                 break;
172             case THING_TYPE_ID_RSP1098:
173                 model = RotelModel.RSP1098;
174                 break;
175             case THING_TYPE_ID_RSP1570:
176                 model = RotelModel.RSP1570;
177                 break;
178             case THING_TYPE_ID_RSP1572:
179                 model = RotelModel.RSP1572;
180                 break;
181             case THING_TYPE_ID_RSX1055:
182                 model = RotelModel.RSX1055;
183                 break;
184             case THING_TYPE_ID_RSX1056:
185                 model = RotelModel.RSX1056;
186                 break;
187             case THING_TYPE_ID_RSX1057:
188                 model = RotelModel.RSX1057;
189                 break;
190             case THING_TYPE_ID_RSX1058:
191                 model = RotelModel.RSX1058;
192                 break;
193             case THING_TYPE_ID_RSX1065:
194                 model = RotelModel.RSX1065;
195                 break;
196             case THING_TYPE_ID_RSX1067:
197                 model = RotelModel.RSX1067;
198                 break;
199             case THING_TYPE_ID_RSX1550:
200                 model = RotelModel.RSX1550;
201                 break;
202             case THING_TYPE_ID_RSX1560:
203                 model = RotelModel.RSX1560;
204                 break;
205             case THING_TYPE_ID_RSX1562:
206                 model = RotelModel.RSX1562;
207                 break;
208             case THING_TYPE_ID_A11:
209                 model = RotelModel.A11;
210                 break;
211             case THING_TYPE_ID_A12:
212                 model = RotelModel.A12;
213                 break;
214             case THING_TYPE_ID_A14:
215                 model = RotelModel.A14;
216                 break;
217             case THING_TYPE_ID_CD11:
218                 model = RotelModel.CD11;
219                 break;
220             case THING_TYPE_ID_CD14:
221                 model = RotelModel.CD14;
222                 break;
223             case THING_TYPE_ID_RA11:
224                 model = RotelModel.RA11;
225                 break;
226             case THING_TYPE_ID_RA12:
227                 model = RotelModel.RA12;
228                 break;
229             case THING_TYPE_ID_RA1570:
230                 model = RotelModel.RA1570;
231                 break;
232             case THING_TYPE_ID_RA1572:
233                 model = RotelModel.RA1572;
234                 break;
235             case THING_TYPE_ID_RA1592:
236                 model = RotelModel.RA1592;
237                 break;
238             case THING_TYPE_ID_RAP1580:
239                 model = RotelModel.RAP1580;
240                 break;
241             case THING_TYPE_ID_RC1570:
242                 model = RotelModel.RC1570;
243                 break;
244             case THING_TYPE_ID_RC1572:
245                 model = RotelModel.RC1572;
246                 break;
247             case THING_TYPE_ID_RC1590:
248                 model = RotelModel.RC1590;
249                 break;
250             case THING_TYPE_ID_RCD1570:
251                 model = RotelModel.RCD1570;
252                 break;
253             case THING_TYPE_ID_RCD1572:
254                 model = RotelModel.RCD1572;
255                 break;
256             case THING_TYPE_ID_RCX1500:
257                 model = RotelModel.RCX1500;
258                 break;
259             case THING_TYPE_ID_RDD1580:
260                 model = RotelModel.RDD1580;
261                 break;
262             case THING_TYPE_ID_RDG1520:
263             case THING_TYPE_ID_RT09:
264                 model = RotelModel.RDG1520;
265                 break;
266             case THING_TYPE_ID_RSP1576:
267                 model = RotelModel.RSP1576;
268                 break;
269             case THING_TYPE_ID_RSP1582:
270                 model = RotelModel.RSP1582;
271                 break;
272             case THING_TYPE_ID_RT11:
273                 model = RotelModel.RT11;
274                 break;
275             case THING_TYPE_ID_RT1570:
276                 model = RotelModel.RT1570;
277                 break;
278             case THING_TYPE_ID_T11:
279                 model = RotelModel.T11;
280                 break;
281             case THING_TYPE_ID_T14:
282                 model = RotelModel.T14;
283                 break;
284             case THING_TYPE_ID_M8:
285                 model = RotelModel.M8;
286                 break;
287             case THING_TYPE_ID_P5:
288                 model = RotelModel.P5;
289                 break;
290             case THING_TYPE_ID_S5:
291                 model = RotelModel.S5;
292                 break;
293             case THING_TYPE_ID_X3:
294                 model = RotelModel.X3;
295                 break;
296             case THING_TYPE_ID_X5:
297                 model = RotelModel.X5;
298                 break;
299             default:
300                 model = DEFAULT_MODEL;
301                 break;
302         }
303
304         RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
305
306         protocol = RotelProtocol.HEX;
307         if (config.protocol != null && !config.protocol.isEmpty()) {
308             try {
309                 protocol = RotelProtocol.getFromName(config.protocol);
310             } catch (RotelException e) {
311                 // Invalid protocol name in configuration, HEX will be considered by default
312             }
313         } else {
314             Map<String, String> properties = editProperties();
315             String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
316             if (property != null && !property.isEmpty()) {
317                 try {
318                     protocol = RotelProtocol.getFromName(property);
319                 } catch (RotelException e) {
320                     // Invalid protocol name in thing property, HEX will be considered by default
321                 }
322             }
323         }
324         logger.debug("rotelProtocol {}", protocol.getName());
325
326         Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
327         Map<RotelSource, String> sourcesLabels = new HashMap<>();
328
329         String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
330
331         if (model.hasVolumeControl()) {
332             maxVolume = model.getVolumeMax();
333             if (!model.hasDirectVolumeControl()) {
334                 logger.info(
335                         "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
336                         minVolume, maxVolume);
337             }
338         }
339         if (model.hasToneControl()) {
340             maxToneLevel = model.getToneLevelMax();
341             minToneLevel = -maxToneLevel;
342             logger.info(
343                     "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
344                     minToneLevel, maxToneLevel);
345         }
346         if (model.hasBalanceControl()) {
347             maxBalanceLevel = model.getBalanceLevelMax();
348             minBalanceLevel = -maxBalanceLevel;
349             logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
350                     minBalanceLevel, maxBalanceLevel);
351         }
352
353         // Check configuration settings
354         String configError = null;
355         if ((config.serialPort == null || config.serialPort.isEmpty())
356                 && (config.host == null || config.host.isEmpty())) {
357             configError = "@text/offline.config-error-unknown-serialport-and-host";
358         } else if (config.host == null || config.host.isEmpty()) {
359             if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
360                 configError = "@text/offline.config-error-invalid-serial-over-ip";
361             }
362         } else {
363             if (config.port == null) {
364                 configError = "@text/offline.config-error-unknown-port";
365             } else if (config.port <= 0) {
366                 configError = "@text/offline.config-error-invalid-port";
367             }
368         }
369
370         if (configError != null) {
371             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
372         } else {
373             for (RotelSource src : model.getSources()) {
374                 // Consider custom input labels
375                 String label = null;
376                 switch (src.getName()) {
377                     case "CD":
378                         label = config.inputLabelCd;
379                         break;
380                     case "TUNER":
381                         label = config.inputLabelTuner;
382                         break;
383                     case "TAPE":
384                         label = config.inputLabelTape;
385                         break;
386                     case "PHONO":
387                         label = config.inputLabelPhono;
388                         break;
389                     case "VIDEO1":
390                         label = config.inputLabelVideo1;
391                         break;
392                     case "VIDEO2":
393                         label = config.inputLabelVideo2;
394                         break;
395                     case "VIDEO3":
396                         label = config.inputLabelVideo3;
397                         break;
398                     case "VIDEO4":
399                         label = config.inputLabelVideo4;
400                         break;
401                     case "VIDEO5":
402                         label = config.inputLabelVideo5;
403                         break;
404                     case "VIDEO6":
405                         label = config.inputLabelVideo6;
406                         break;
407                     case "USB":
408                         label = config.inputLabelUsb;
409                         break;
410                     case "MULTI":
411                         label = config.inputLabelMulti;
412                         break;
413                     default:
414                         break;
415                 }
416                 if (label != null && !label.isEmpty()) {
417                     sourcesCustomLabels.put(src, label);
418                 }
419                 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
420             }
421
422             if (protocol == RotelProtocol.HEX) {
423                 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
424             } else if (protocol == RotelProtocol.ASCII_V1) {
425                 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
426             } else {
427                 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
428             }
429
430             if (USE_SIMULATED_DEVICE) {
431                 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
432             } else if (config.serialPort != null) {
433                 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
434                         protocolHandler, readerThreadName);
435             } else {
436                 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
437             }
438
439             if (model.hasSourceControl()) {
440                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
441                         getStateOptions(model.getSources(), sourcesCustomLabels));
442                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
443                         getStateOptions(model.getSources(), sourcesCustomLabels));
444                 stateDescriptionProvider.setStateOptions(
445                         new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
446                         getStateOptions(model.getRecordSources(), sourcesCustomLabels));
447             }
448             if (model.hasZone2SourceControl()) {
449                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
450                         getStateOptions(model.getZone2Sources(), sourcesCustomLabels));
451             }
452             if (model.hasZone3SourceControl()) {
453                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
454                         getStateOptions(model.getZone3Sources(), sourcesCustomLabels));
455             }
456             if (model.hasZone4SourceControl()) {
457                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
458                         getStateOptions(model.getZone4Sources(), sourcesCustomLabels));
459             }
460             if (model.hasDspControl()) {
461                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
462                         model.getDspStateOptions());
463                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
464                         model.getDspStateOptions());
465             }
466
467             updateStatus(ThingStatus.UNKNOWN);
468
469             scheduleReconnectJob();
470         }
471
472         logger.debug("Finished initializing!");
473     }
474
475     @Override
476     public void dispose() {
477         logger.debug("Disposing handler for thing {}", getThing().getUID());
478         cancelPowerOffJob();
479         cancelPowerOnJob();
480         cancelPowerOnZone2Job();
481         cancelPowerOnZone3Job();
482         cancelPowerOnZone4Job();
483         cancelReconnectJob();
484         closeConnection();
485         super.dispose();
486     }
487
488     public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
489         List<StateOption> options = new ArrayList<>();
490         for (RotelSource item : list) {
491             String label = sourcesLabels.get(item);
492             options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
493         }
494         return options;
495     }
496
497     @Override
498     public void handleCommand(ChannelUID channelUID, Command command) {
499         String channel = channelUID.getId();
500
501         if (getThing().getStatus() != ThingStatus.ONLINE) {
502             logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
503             return;
504         }
505
506         if (command instanceof RefreshType) {
507             updateChannelState(channel);
508             return;
509         }
510
511         if (!connector.isConnected()) {
512             logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
513             return;
514         }
515
516         RotelSource src;
517         RotelCommand cmd;
518         boolean success = true;
519         synchronized (sequenceLock) {
520             try {
521                 switch (channel) {
522                     case CHANNEL_POWER:
523                     case CHANNEL_MAIN_POWER:
524                         handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
525                         break;
526                     case CHANNEL_ZONE2_POWER:
527                         if (model.hasZone2Commands()) {
528                             handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
529                         } else if (model.getNbAdditionalZones() == 1) {
530                             if (isPowerOn() || powerZone2) {
531                                 selectZone(2, model.getZoneSelectCmd());
532                             }
533                             sendCommand(RotelCommand.ZONE_SELECT);
534                         } else {
535                             success = false;
536                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
537                         }
538                         break;
539                     case CHANNEL_ZONE3_POWER:
540                         if (model.hasZone3Commands()) {
541                             handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
542                         } else {
543                             success = false;
544                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
545                         }
546                         break;
547                     case CHANNEL_ZONE4_POWER:
548                         if (model.hasZone4Commands()) {
549                             handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
550                         } else {
551                             success = false;
552                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
553                         }
554                         break;
555                     case CHANNEL_SOURCE:
556                     case CHANNEL_MAIN_SOURCE:
557                         if (!isPowerOn()) {
558                             success = false;
559                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
560                         } else {
561                             src = model.getSourceFromName(command.toString());
562                             cmd = model.hasOtherThanPrimaryCommands() ? src.getMainZoneCommand() : src.getCommand();
563                             if (cmd != null) {
564                                 sendCommand(cmd);
565                                 if (model.canGetFrequency()) {
566                                     // send <new-source> returns
567                                     // 1.) the selected <new-source>
568                                     // 2.) the used frequency
569                                     // BUT:
570                                     // at response-time the frequency has the value of <old-source>
571                                     // so we must wait a short moment to get the frequency of <new-source>
572                                     Thread.sleep(1000);
573                                     sendCommand(RotelCommand.FREQUENCY);
574                                     Thread.sleep(100);
575                                     updateChannelState(CHANNEL_FREQUENCY);
576                                 }
577                             } else {
578                                 success = false;
579                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
580                                         channel);
581                             }
582                         }
583                         break;
584                     case CHANNEL_MAIN_RECORD_SOURCE:
585                         if (!isPowerOn()) {
586                             success = false;
587                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
588                         } else if (model.hasOtherThanPrimaryCommands()) {
589                             src = model.getSourceFromName(command.toString());
590                             cmd = src.getRecordCommand();
591                             if (cmd != null) {
592                                 sendCommand(cmd);
593                             } else {
594                                 success = false;
595                                 logger.debug("Command {} from channel {} failed: undefined record source command",
596                                         command, channel);
597                             }
598                         } else {
599                             src = model.getSourceFromName(command.toString());
600                             cmd = src.getCommand();
601                             if (cmd != null) {
602                                 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
603                                 Thread.sleep(100);
604                                 sendCommand(cmd);
605                             } else {
606                                 success = false;
607                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
608                                         channel);
609                             }
610                         }
611                         break;
612                     case CHANNEL_ZONE2_SOURCE:
613                         if (!powerZone2) {
614                             success = false;
615                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
616                         } else if (model.hasZone2Commands()) {
617                             src = model.getSourceFromName(command.toString());
618                             cmd = src.getZone2Command();
619                             if (cmd != null) {
620                                 sendCommand(cmd);
621                             } else {
622                                 success = false;
623                                 logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
624                                         command, channel);
625                             }
626                         } else if (model.getNbAdditionalZones() >= 1) {
627                             src = model.getSourceFromName(command.toString());
628                             cmd = src.getCommand();
629                             if (cmd != null) {
630                                 selectZone(2, model.getZoneSelectCmd());
631                                 sendCommand(cmd);
632                             } else {
633                                 success = false;
634                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
635                                         channel);
636                             }
637                         } else {
638                             success = false;
639                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
640                         }
641                         break;
642                     case CHANNEL_ZONE3_SOURCE:
643                         if (!powerZone3) {
644                             success = false;
645                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
646                         } else if (model.hasZone3Commands()) {
647                             src = model.getSourceFromName(command.toString());
648                             cmd = src.getZone3Command();
649                             if (cmd != null) {
650                                 sendCommand(cmd);
651                             } else {
652                                 success = false;
653                                 logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
654                                         command, channel);
655                             }
656                         } else {
657                             success = false;
658                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
659                         }
660                         break;
661                     case CHANNEL_ZONE4_SOURCE:
662                         if (!powerZone4) {
663                             success = false;
664                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
665                         } else if (model.hasZone4Commands()) {
666                             src = model.getSourceFromName(command.toString());
667                             cmd = src.getZone4Command();
668                             if (cmd != null) {
669                                 sendCommand(cmd);
670                             } else {
671                                 success = false;
672                                 logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
673                                         command, channel);
674                             }
675                         } else {
676                             success = false;
677                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
678                         }
679                         break;
680                     case CHANNEL_DSP:
681                     case CHANNEL_MAIN_DSP:
682                         if (!isPowerOn()) {
683                             success = false;
684                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
685                         } else {
686                             sendCommand(model.getCommandFromDspName(command.toString()));
687                         }
688                         break;
689                     case CHANNEL_VOLUME:
690                     case CHANNEL_MAIN_VOLUME:
691                         if (!isPowerOn()) {
692                             success = false;
693                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
694                         } else if (model.hasVolumeControl()) {
695                             handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
696                                     RotelCommand.VOLUME_SET);
697                         } else {
698                             success = false;
699                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
700                         }
701                         break;
702                     case CHANNEL_MAIN_VOLUME_UP_DOWN:
703                         if (!isPowerOn()) {
704                             success = false;
705                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
706                         } else if (model.hasVolumeControl()) {
707                             handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
708                                     null);
709                         } else {
710                             success = false;
711                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
712                         }
713                         break;
714                     case CHANNEL_ZONE2_VOLUME:
715                         if (!powerZone2) {
716                             success = false;
717                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
718                         } else if (fixedVolumeZone2) {
719                             success = false;
720                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
721                                     channel);
722                         } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
723                             if (model.hasZone2Commands()) {
724                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
725                                         RotelCommand.ZONE2_VOLUME_DOWN, RotelCommand.ZONE2_VOLUME_SET);
726                             } else {
727                                 selectZone(2, model.getZoneSelectCmd());
728                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
729                                         RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
730                             }
731                         } else {
732                             success = false;
733                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
734                         }
735                         break;
736                     case CHANNEL_ZONE2_VOLUME_UP_DOWN:
737                         if (!powerZone2) {
738                             success = false;
739                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
740                         } else if (fixedVolumeZone2) {
741                             success = false;
742                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
743                                     channel);
744                         } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
745                             if (model.hasZone2Commands()) {
746                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
747                                         RotelCommand.ZONE2_VOLUME_DOWN, null);
748                             } else {
749                                 selectZone(2, model.getZoneSelectCmd());
750                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
751                                         RotelCommand.VOLUME_DOWN, null);
752                             }
753                         } else {
754                             success = false;
755                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
756                         }
757                         break;
758                     case CHANNEL_ZONE3_VOLUME:
759                         if (!powerZone3) {
760                             success = false;
761                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
762                         } else if (fixedVolumeZone3) {
763                             success = false;
764                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
765                                     channel);
766                         } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
767                             handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
768                                     RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
769                         } else {
770                             success = false;
771                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
772                         }
773                         break;
774                     case CHANNEL_ZONE4_VOLUME:
775                         if (!powerZone4) {
776                             success = false;
777                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
778                         } else if (fixedVolumeZone4) {
779                             success = false;
780                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
781                                     channel);
782                         } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
783                             handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
784                                     RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
785                         } else {
786                             success = false;
787                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
788                         }
789                         break;
790                     case CHANNEL_MUTE:
791                     case CHANNEL_MAIN_MUTE:
792                         if (!isPowerOn()) {
793                             success = false;
794                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
795                         } else if (model.hasVolumeControl()) {
796                             handleMuteCmd(protocol == RotelProtocol.HEX, channel, command, getMuteOnCommand(),
797                                     getMuteOffCommand(), getMuteToggleCommand());
798                         } else {
799                             success = false;
800                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
801                         }
802                         break;
803                     case CHANNEL_ZONE2_MUTE:
804                         if (!powerZone2) {
805                             success = false;
806                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
807                         } else if (model.hasVolumeControl() && model.hasZone2Commands()) {
808                             handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
809                                     RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
810                         } else {
811                             success = false;
812                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
813                         }
814                         break;
815                     case CHANNEL_ZONE3_MUTE:
816                         if (!powerZone3) {
817                             success = false;
818                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
819                         } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
820                             handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
821                                     RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
822                         } else {
823                             success = false;
824                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
825                         }
826                         break;
827                     case CHANNEL_ZONE4_MUTE:
828                         if (!powerZone4) {
829                             success = false;
830                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
831                         } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
832                             handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
833                                     RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
834                         } else {
835                             success = false;
836                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
837                         }
838                         break;
839                     case CHANNEL_BASS:
840                     case CHANNEL_MAIN_BASS:
841                         if (!isPowerOn()) {
842                             success = false;
843                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
844                         } else if (tcbypass) {
845                             logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
846                                     channel);
847                             updateChannelState(CHANNEL_BASS);
848                         } else {
849                             handleToneCmd(bass, channel, command, 2, RotelCommand.BASS_UP, RotelCommand.BASS_DOWN,
850                                     RotelCommand.BASS_SET);
851                         }
852                         break;
853                     case CHANNEL_TREBLE:
854                     case CHANNEL_MAIN_TREBLE:
855                         if (!isPowerOn()) {
856                             success = false;
857                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
858                         } else if (tcbypass) {
859                             logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
860                                     channel);
861                             updateChannelState(CHANNEL_TREBLE);
862                         } else {
863                             handleToneCmd(treble, channel, command, 1, RotelCommand.TREBLE_UP, RotelCommand.TREBLE_DOWN,
864                                     RotelCommand.TREBLE_SET);
865                         }
866                         break;
867                     case CHANNEL_PLAY_CONTROL:
868                         if (!isPowerOn()) {
869                             success = false;
870                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
871                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
872                             sendCommand(RotelCommand.PLAY);
873                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
874                             sendCommand(RotelCommand.PAUSE);
875                             if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
876                                     && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
877                                 Thread.sleep(SLEEP_INTV);
878                                 sendCommand(RotelCommand.PLAY_STATUS);
879                             }
880                         } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
881                             sendCommand(RotelCommand.TRACK_FORWARD);
882                         } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
883                             sendCommand(RotelCommand.TRACK_BACKWORD);
884                         } else {
885                             success = false;
886                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
887                         }
888                         break;
889                     case CHANNEL_BRIGHTNESS:
890                         if (!isPowerOn()) {
891                             success = false;
892                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
893                         } else if (!model.hasDimmerControl()) {
894                             success = false;
895                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
896                         } else if (command instanceof PercentType) {
897                             int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
898                                     * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
899                                     + model.getDimmerLevelMin();
900                             sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
901                         } else {
902                             success = false;
903                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
904                         }
905                         break;
906                     case CHANNEL_TCBYPASS:
907                         if (!isPowerOn()) {
908                             success = false;
909                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
910                         } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
911                             success = false;
912                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
913                         } else {
914                             handleTcbypassCmd(channel, command,
915                                     protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
916                                             : RotelCommand.TCBYPASS_ON,
917                                     protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
918                                             : RotelCommand.TCBYPASS_OFF);
919                         }
920                         break;
921                     case CHANNEL_BALANCE:
922                         if (!isPowerOn()) {
923                             success = false;
924                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
925                         } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
926                             success = false;
927                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
928                         } else {
929                             handleBalanceCmd(channel, command, RotelCommand.BALANCE_LEFT, RotelCommand.BALANCE_RIGHT,
930                                     RotelCommand.BALANCE_SET);
931                         }
932                         break;
933                     case CHANNEL_SPEAKER_A:
934                         if (!isPowerOn()) {
935                             success = false;
936                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
937                         } else {
938                             handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
939                                     RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
940                         }
941                         break;
942                     case CHANNEL_SPEAKER_B:
943                         if (!isPowerOn()) {
944                             success = false;
945                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
946                         } else {
947                             handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
948                                     RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
949                         }
950                         break;
951                     default:
952                         success = false;
953                         logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
954                         break;
955                 }
956                 if (success) {
957                     logger.debug("Command {} from channel {} succeeded", command, channel);
958                 } else {
959                     updateChannelState(channel);
960                 }
961             } catch (RotelException e) {
962                 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
963                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
964                         "@text/offline.comm-error-sending-command");
965                 closeConnection();
966                 scheduleReconnectJob();
967             } catch (InterruptedException e) {
968                 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
969                 Thread.currentThread().interrupt();
970             }
971         }
972     }
973
974     /**
975      * Handle a power ON/OFF command
976      *
977      * @param channel the channel
978      * @param command the received channel command (OnOffType)
979      * @param onCmd the command to be sent to the device to power it ON
980      * @param offCmd the command to be sent to the device to power it OFF
981      *
982      * @throws RotelException in case of communication error with the device
983      */
984     private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
985             throws RotelException {
986         if (command instanceof OnOffType && command == OnOffType.ON) {
987             sendCommand(onCmd);
988         } else if (command instanceof OnOffType && command == OnOffType.OFF) {
989             sendCommand(offCmd);
990         } else {
991             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
992         }
993     }
994
995     /**
996      * Handle a volume command
997      *
998      * @param current the current volume
999      * @param channel the channel
1000      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1001      * @param upCmd the command to be sent to the device to increase the volume
1002      * @param downCmd the command to be sent to the device to decrease the volume
1003      * @param setCmd the command to be sent to the device to set the volume at a value
1004      *
1005      * @throws RotelException in case of communication error with the device
1006      */
1007     private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
1008             @Nullable RotelCommand setCmd) throws RotelException {
1009         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1010             sendCommand(upCmd);
1011         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1012             sendCommand(downCmd);
1013         } else if (command instanceof DecimalType && setCmd == null) {
1014             int value = ((DecimalType) command).intValue();
1015             if (value >= minVolume && value <= maxVolume) {
1016                 if (value > current) {
1017                     sendCommand(upCmd);
1018                 } else if (value < current) {
1019                     sendCommand(downCmd);
1020                 }
1021             }
1022         } else if (command instanceof PercentType && setCmd != null) {
1023             int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1024                     + minVolume;
1025             sendCommand(setCmd, value);
1026         } else {
1027             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1028         }
1029     }
1030
1031     /**
1032      * Handle a mute command
1033      *
1034      * @param onlyToggle true if only the toggle command must be used
1035      * @param channel the channel
1036      * @param command the received channel command (OnOffType)
1037      * @param onCmd the command to be sent to the device to mute
1038      * @param offCmd the command to be sent to the device to unmute
1039      * @param toggleCmd the command to be sent to the device to toggle the mute state
1040      *
1041      * @throws RotelException in case of communication error with the device
1042      */
1043     private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1044             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1045         if (command instanceof OnOffType) {
1046             if (onlyToggle) {
1047                 sendCommand(toggleCmd);
1048             } else if (command == OnOffType.ON) {
1049                 sendCommand(onCmd);
1050             } else if (command == OnOffType.OFF) {
1051                 sendCommand(offCmd);
1052             }
1053         } else {
1054             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1055         }
1056     }
1057
1058     /**
1059      * Handle a tone level adjustment command (bass or treble)
1060      *
1061      * @param current the current tone level
1062      * @param channel the channel
1063      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1064      * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1065      * @param upCmd the command to be sent to the device to increase the tone level
1066      * @param downCmd the command to be sent to the device to decrease the tone level
1067      * @param setCmd the command to be sent to the device to set the tone level at a value
1068      *
1069      * @throws RotelException in case of communication error with the device
1070      * @throws InterruptedException in case of interruption during a thread sleep
1071      */
1072     private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1073             RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1074         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1075             selectToneControl(nbSelect);
1076             sendCommand(upCmd);
1077         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1078             selectToneControl(nbSelect);
1079             sendCommand(downCmd);
1080         } else if (command instanceof DecimalType) {
1081             int value = ((DecimalType) command).intValue();
1082             if (value >= minToneLevel && value <= maxToneLevel) {
1083                 if (protocol != RotelProtocol.HEX) {
1084                     sendCommand(setCmd, value);
1085                 } else if (value > current) {
1086                     selectToneControl(nbSelect);
1087                     sendCommand(upCmd);
1088                 } else if (value < current) {
1089                     selectToneControl(nbSelect);
1090                     sendCommand(downCmd);
1091                 }
1092             }
1093         } else {
1094             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1095         }
1096     }
1097
1098     /**
1099      * Handle a tcbypass command (only for ASCII protocol)
1100      *
1101      * @param channel the channel
1102      * @param command the received channel command (OnOffType)
1103      * @param onCmd the command to be sent to the device to bypass_on
1104      * @param offCmd the command to be sent to the device to bypass_off
1105      *
1106      * @throws RotelException in case of communication error with the device
1107      */
1108     private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1109             throws RotelException, InterruptedException {
1110         if (command instanceof OnOffType) {
1111             if (command == OnOffType.ON) {
1112                 sendCommand(onCmd);
1113                 bass = 0;
1114                 treble = 0;
1115                 updateChannelState(CHANNEL_BASS);
1116                 updateChannelState(CHANNEL_TREBLE);
1117             } else if (command == OnOffType.OFF) {
1118                 sendCommand(offCmd);
1119                 Thread.sleep(200);
1120                 sendCommand(RotelCommand.BASS);
1121                 Thread.sleep(200);
1122                 sendCommand(RotelCommand.TREBLE);
1123             }
1124         } else {
1125             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1126         }
1127     }
1128
1129     /**
1130      * Handle a speaker command
1131      *
1132      * @param onlyToggle true if only the toggle command must be used
1133      * @param channel the channel
1134      * @param command the received channel command (OnOffType)
1135      * @param onCmd the command to be sent to the device to speaker_x_on
1136      * @param offCmd the command to be sent to the device to speaker_x_off
1137      * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1138      *
1139      * @throws RotelException in case of communication error with the device
1140      */
1141     private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1142             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1143         if (command instanceof OnOffType) {
1144             if (onlyToggle) {
1145                 sendCommand(toggleCmd);
1146             } else if (command == OnOffType.ON) {
1147                 sendCommand(onCmd);
1148             } else if (command == OnOffType.OFF) {
1149                 sendCommand(offCmd);
1150             }
1151         } else {
1152             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1153         }
1154     }
1155
1156     /**
1157      * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1158      *
1159      * @param channel the channel
1160      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1161      * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1162      * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1163      * @param setCmd the command to be sent to the device to set the balance at a value
1164      *
1165      * @throws RotelException in case of communication error with the device
1166      * @throws InterruptedException in case of interruption during a thread sleep
1167      */
1168     private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1169             RotelCommand setCmd) throws RotelException, InterruptedException {
1170         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1171             sendCommand(rightCmd);
1172         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1173             sendCommand(leftCmd);
1174         } else if (command instanceof DecimalType) {
1175             int value = ((DecimalType) command).intValue();
1176             if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1177                 sendCommand(setCmd, value);
1178             }
1179         } else {
1180             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1181         }
1182     }
1183
1184     /**
1185      * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1186      *
1187      * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1188      *
1189      * @throws RotelException in case of communication error with the device
1190      * @throws InterruptedException in case of interruption during a thread sleep
1191      */
1192     private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1193         // No tone control select command for RSX-1065
1194         if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1195             selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1196         }
1197     }
1198
1199     /**
1200      * Run a sequence of commands to display a particular zone on the device front panel
1201      *
1202      * @param zone the zone to be displayed (1 for main zone)
1203      * @param selectCommand the command to be sent to the device to switch the display between zones
1204      *
1205      * @throws RotelException in case of communication error with the device
1206      * @throws InterruptedException in case of interruption during a thread sleep
1207      */
1208     private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1209             throws RotelException, InterruptedException {
1210         if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1 && zone >= 1 && zone != currentZone
1211                 && selectCommand != null) {
1212             int nbSelect;
1213             if (zone < currentZone) {
1214                 nbSelect = zone + model.getNbAdditionalZones() - currentZone;
1215                 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1216                     nbSelect++;
1217                 }
1218             } else {
1219                 nbSelect = zone - currentZone;
1220                 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1221                         && !selectingRecord) {
1222                     nbSelect++;
1223                 }
1224             }
1225             selectFeature(nbSelect, null, selectCommand);
1226         }
1227     }
1228
1229     /**
1230      * Run a sequence of commands to display a particular feature on the device front panel
1231      *
1232      * @param nbSelect the number of select commands to be run
1233      * @param preCmd the initial command to be sent to the device (before the select commands)
1234      * @param selectCmd the select command to be sent to the device
1235      *
1236      * @throws RotelException in case of communication error with the device
1237      * @throws InterruptedException in case of interruption during a thread sleep
1238      */
1239     private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1240             throws RotelException, InterruptedException {
1241         if (protocol == RotelProtocol.HEX) {
1242             if (preCmd != null) {
1243                 sendCommand(preCmd);
1244                 Thread.sleep(100);
1245             }
1246             for (int i = 1; i <= nbSelect; i++) {
1247                 sendCommand(selectCmd);
1248                 Thread.sleep(200);
1249             }
1250         }
1251     }
1252
1253     /**
1254      * Open the connection with the Rotel device
1255      *
1256      * @return true if the connection is opened successfully or flase if not
1257      */
1258     private synchronized boolean openConnection() {
1259         protocolHandler.addEventListener(this);
1260         try {
1261             connector.open();
1262         } catch (RotelException e) {
1263             logger.debug("openConnection() failed", e);
1264         }
1265         logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1266         return connector.isConnected();
1267     }
1268
1269     /**
1270      * Close the connection with the Rotel device
1271      */
1272     private synchronized void closeConnection() {
1273         connector.close();
1274         protocolHandler.removeEventListener(this);
1275         logger.debug("closeConnection(): disconnected");
1276     }
1277
1278     @Override
1279     public void onNewMessageEvent(EventObject event) {
1280         cancelPowerOffJob();
1281
1282         RotelMessageEvent evt = (RotelMessageEvent) event;
1283         logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1284
1285         String key = evt.getKey();
1286         String value = evt.getValue().trim();
1287         if (!KEY_ERROR.equals(key)) {
1288             updateStatus(ThingStatus.ONLINE);
1289         }
1290         try {
1291             switch (key) {
1292                 case KEY_ERROR:
1293                     logger.debug("Reading feedback message failed");
1294                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1295                             "@text/offline.comm-error-reading-thread");
1296                     closeConnection();
1297                     break;
1298                 case KEY_LINE1:
1299                     frontPanelLine1 = value;
1300                     updateChannelState(CHANNEL_LINE1);
1301                     break;
1302                 case KEY_LINE2:
1303                     frontPanelLine2 = value;
1304                     updateChannelState(CHANNEL_LINE2);
1305                     break;
1306                 case KEY_ZONE:
1307                     currentZone = Integer.parseInt(value);
1308                     break;
1309                 case KEY_RECORD_SEL:
1310                     selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1311                     break;
1312                 case KEY_POWER:
1313                     if (POWER_ON.equalsIgnoreCase(value)) {
1314                         handlePowerOn();
1315                     } else if (STANDBY.equalsIgnoreCase(value)) {
1316                         handlePowerOff();
1317                     } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1318                         schedulePowerOffJob(false);
1319                     } else {
1320                         throw new RotelException("Invalid value");
1321                     }
1322                     break;
1323                 case KEY_POWER_ZONE2:
1324                     if (POWER_ON.equalsIgnoreCase(value)) {
1325                         handlePowerOnZone2();
1326                     } else if (STANDBY.equalsIgnoreCase(value)) {
1327                         handlePowerOffZone2();
1328                     } else {
1329                         throw new RotelException("Invalid value");
1330                     }
1331                     break;
1332                 case KEY_POWER_ZONE3:
1333                     if (POWER_ON.equalsIgnoreCase(value)) {
1334                         handlePowerOnZone3();
1335                     } else if (STANDBY.equalsIgnoreCase(value)) {
1336                         handlePowerOffZone3();
1337                     } else {
1338                         throw new RotelException("Invalid value");
1339                     }
1340                     break;
1341                 case KEY_POWER_ZONE4:
1342                     if (POWER_ON.equalsIgnoreCase(value)) {
1343                         handlePowerOnZone4();
1344                     } else if (STANDBY.equalsIgnoreCase(value)) {
1345                         handlePowerOffZone4();
1346                     } else {
1347                         throw new RotelException("Invalid value");
1348                     }
1349                     break;
1350                 case KEY_VOLUME_MIN:
1351                     minVolume = Integer.parseInt(value);
1352                     if (!model.hasDirectVolumeControl()) {
1353                         logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1354                                 minVolume);
1355                     }
1356                     break;
1357                 case KEY_VOLUME_MAX:
1358                     maxVolume = Integer.parseInt(value);
1359                     if (!model.hasDirectVolumeControl()) {
1360                         logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1361                                 maxVolume);
1362                     }
1363                     break;
1364                 case KEY_VOLUME:
1365                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1366                         volume = minVolume;
1367                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1368                         volume = maxVolume;
1369                     } else {
1370                         volume = Integer.parseInt(value);
1371                     }
1372                     updateChannelState(CHANNEL_VOLUME);
1373                     updateChannelState(CHANNEL_MAIN_VOLUME);
1374                     updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1375                     break;
1376                 case KEY_MUTE:
1377                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1378                         mute = true;
1379                         updateChannelState(CHANNEL_MUTE);
1380                         updateChannelState(CHANNEL_MAIN_MUTE);
1381                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1382                         mute = false;
1383                         updateChannelState(CHANNEL_MUTE);
1384                         updateChannelState(CHANNEL_MAIN_MUTE);
1385                     } else {
1386                         throw new RotelException("Invalid value");
1387                     }
1388                     break;
1389                 case KEY_VOLUME_ZONE2:
1390                     fixedVolumeZone2 = false;
1391                     if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1392                         fixedVolumeZone2 = true;
1393                     } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1394                         volumeZone2 = minVolume;
1395                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1396                         volumeZone2 = maxVolume;
1397                     } else {
1398                         volumeZone2 = Integer.parseInt(value);
1399                     }
1400                     updateChannelState(CHANNEL_ZONE2_VOLUME);
1401                     updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1402                     break;
1403                 case KEY_VOLUME_ZONE3:
1404                     fixedVolumeZone3 = false;
1405                     if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1406                         fixedVolumeZone3 = true;
1407                     } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1408                         volumeZone3 = minVolume;
1409                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1410                         volumeZone3 = maxVolume;
1411                     } else {
1412                         volumeZone3 = Integer.parseInt(value);
1413                     }
1414                     updateChannelState(CHANNEL_ZONE3_VOLUME);
1415                     break;
1416                 case KEY_VOLUME_ZONE4:
1417                     fixedVolumeZone4 = false;
1418                     if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1419                         fixedVolumeZone4 = true;
1420                     } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1421                         volumeZone4 = minVolume;
1422                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1423                         volumeZone4 = maxVolume;
1424                     } else {
1425                         volumeZone4 = Integer.parseInt(value);
1426                     }
1427                     updateChannelState(CHANNEL_ZONE4_VOLUME);
1428                     break;
1429                 case KEY_MUTE_ZONE2:
1430                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1431                         muteZone2 = true;
1432                         updateChannelState(CHANNEL_ZONE2_MUTE);
1433                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1434                         muteZone2 = false;
1435                         updateChannelState(CHANNEL_ZONE2_MUTE);
1436                     } else {
1437                         throw new RotelException("Invalid value");
1438                     }
1439                     break;
1440                 case KEY_MUTE_ZONE3:
1441                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1442                         muteZone3 = true;
1443                         updateChannelState(CHANNEL_ZONE3_MUTE);
1444                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1445                         muteZone3 = false;
1446                         updateChannelState(CHANNEL_ZONE3_MUTE);
1447                     } else {
1448                         throw new RotelException("Invalid value");
1449                     }
1450                     break;
1451                 case KEY_MUTE_ZONE4:
1452                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1453                         muteZone4 = true;
1454                         updateChannelState(CHANNEL_ZONE4_MUTE);
1455                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1456                         muteZone4 = false;
1457                         updateChannelState(CHANNEL_ZONE4_MUTE);
1458                     } else {
1459                         throw new RotelException("Invalid value");
1460                     }
1461                     break;
1462                 case KEY_TONE_MAX:
1463                     maxToneLevel = Integer.parseInt(value);
1464                     minToneLevel = -maxToneLevel;
1465                     logger.info(
1466                             "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1467                             minToneLevel, maxToneLevel);
1468                     break;
1469                 case KEY_BASS:
1470                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1471                         bass = minToneLevel;
1472                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1473                         bass = maxToneLevel;
1474                     } else {
1475                         bass = Integer.parseInt(value);
1476                     }
1477                     updateChannelState(CHANNEL_BASS);
1478                     updateChannelState(CHANNEL_MAIN_BASS);
1479                     break;
1480                 case KEY_TREBLE:
1481                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1482                         treble = minToneLevel;
1483                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1484                         treble = maxToneLevel;
1485                     } else {
1486                         treble = Integer.parseInt(value);
1487                     }
1488                     updateChannelState(CHANNEL_TREBLE);
1489                     updateChannelState(CHANNEL_MAIN_TREBLE);
1490                     break;
1491                 case KEY_SOURCE:
1492                     source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1493                     updateChannelState(CHANNEL_SOURCE);
1494                     updateChannelState(CHANNEL_MAIN_SOURCE);
1495                     break;
1496                 case KEY_RECORD:
1497                     recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1498                     updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1499                     break;
1500                 case KEY_SOURCE_ZONE2:
1501                     sourceZone2 = model.getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1502                     updateChannelState(CHANNEL_ZONE2_SOURCE);
1503                     break;
1504                 case KEY_SOURCE_ZONE3:
1505                     sourceZone3 = model.getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1506                     updateChannelState(CHANNEL_ZONE3_SOURCE);
1507                     break;
1508                 case KEY_SOURCE_ZONE4:
1509                     sourceZone4 = model.getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1510                     updateChannelState(CHANNEL_ZONE4_SOURCE);
1511                     break;
1512                 case KEY_DSP_MODE:
1513                     if ("dolby_pliix_movie".equals(value)) {
1514                         value = "dolby_plii_movie";
1515                     } else if ("dolby_pliix_music".equals(value)) {
1516                         value = "dolby_plii_music";
1517                     } else if ("dolby_pliix_game".equals(value)) {
1518                         value = "dolby_plii_game";
1519                     }
1520                     dsp = model.getDspFromFeedback(value);
1521                     logger.debug("DSP {}", dsp.getName());
1522                     updateChannelState(CHANNEL_DSP);
1523                     updateChannelState(CHANNEL_MAIN_DSP);
1524                     break;
1525                 case KEY1_PLAY_STATUS:
1526                 case KEY2_PLAY_STATUS:
1527                     if (PLAY.equalsIgnoreCase(value)) {
1528                         playStatus = RotelPlayStatus.PLAYING;
1529                         updateChannelState(CHANNEL_PLAY_CONTROL);
1530                     } else if (PAUSE.equalsIgnoreCase(value)) {
1531                         playStatus = RotelPlayStatus.PAUSED;
1532                         updateChannelState(CHANNEL_PLAY_CONTROL);
1533                     } else if (STOP.equalsIgnoreCase(value)) {
1534                         playStatus = RotelPlayStatus.STOPPED;
1535                         updateChannelState(CHANNEL_PLAY_CONTROL);
1536                     } else {
1537                         throw new RotelException("Invalid value");
1538                     }
1539                     break;
1540                 case KEY_TRACK:
1541                     if (source.getName().equals("CD") && !model.hasSourceControl()) {
1542                         track = Integer.parseInt(value);
1543                         updateChannelState(CHANNEL_TRACK);
1544                     }
1545                     break;
1546                 case KEY_FREQ:
1547                     if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1548                         frequency = 0.0;
1549                     } else {
1550                         // Suppress a potential ending "k" or "K"
1551                         if (value.toUpperCase().endsWith("K")) {
1552                             value = value.substring(0, value.length() - 1);
1553                         }
1554                         frequency = Double.parseDouble(value);
1555                     }
1556                     updateChannelState(CHANNEL_FREQUENCY);
1557                     break;
1558                 case KEY_DIMMER:
1559                     brightness = Integer.parseInt(value);
1560                     updateChannelState(CHANNEL_BRIGHTNESS);
1561                     break;
1562                 case KEY_UPDATE_MODE:
1563                 case KEY_DISPLAY_UPDATE:
1564                     break;
1565                 case KEY_TONE:
1566                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1567                         tcbypass = false;
1568                         updateChannelState(CHANNEL_TCBYPASS);
1569                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1570                         tcbypass = true;
1571                         updateChannelState(CHANNEL_TCBYPASS);
1572                     } else {
1573                         throw new RotelException("Invalid value");
1574                     }
1575                     break;
1576                 case KEY_TCBYPASS:
1577                     if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1578                         tcbypass = true;
1579                         updateChannelState(CHANNEL_TCBYPASS);
1580                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1581                         tcbypass = false;
1582                         updateChannelState(CHANNEL_TCBYPASS);
1583                     } else {
1584                         throw new RotelException("Invalid value");
1585                     }
1586                     break;
1587                 case KEY_BALANCE:
1588                     if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1589                         balance = minBalanceLevel;
1590                     } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1591                         balance = maxBalanceLevel;
1592                     } else if (value.toUpperCase().startsWith("L")) {
1593                         balance = -Integer.parseInt(value.substring(1));
1594                     } else if (value.toLowerCase().startsWith("R")) {
1595                         balance = Integer.parseInt(value.substring(1));
1596                     } else {
1597                         balance = Integer.parseInt(value);
1598                     }
1599                     updateChannelState(CHANNEL_BALANCE);
1600                     break;
1601                 case KEY_SPEAKER:
1602                     if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1603                         speakera = true;
1604                         speakerb = false;
1605                         updateChannelState(CHANNEL_SPEAKER_A);
1606                         updateChannelState(CHANNEL_SPEAKER_B);
1607                     } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1608                         speakera = false;
1609                         speakerb = true;
1610                         updateChannelState(CHANNEL_SPEAKER_A);
1611                         updateChannelState(CHANNEL_SPEAKER_B);
1612                     } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1613                         speakera = true;
1614                         speakerb = true;
1615                         updateChannelState(CHANNEL_SPEAKER_A);
1616                         updateChannelState(CHANNEL_SPEAKER_B);
1617                     } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1618                         speakera = false;
1619                         speakerb = false;
1620                         updateChannelState(CHANNEL_SPEAKER_A);
1621                         updateChannelState(CHANNEL_SPEAKER_B);
1622                     } else {
1623                         throw new RotelException("Invalid value");
1624                     }
1625                     break;
1626                 default:
1627                     logger.debug("onNewMessageEvent: unhandled key {}", key);
1628                     break;
1629             }
1630         } catch (NumberFormatException | RotelException e) {
1631             logger.debug("Invalid value {} for key {}", value, key);
1632         }
1633     }
1634
1635     /**
1636      * Handle the received information that device power (main zone) is ON
1637      */
1638     private void handlePowerOn() {
1639         Boolean prev = power;
1640         power = true;
1641         updateChannelState(CHANNEL_POWER);
1642         updateChannelState(CHANNEL_MAIN_POWER);
1643         if ((prev == null) || !prev) {
1644             schedulePowerOnJob();
1645         }
1646     }
1647
1648     /**
1649      * Handle the received information that device power (main zone) is OFF
1650      */
1651     private void handlePowerOff() {
1652         cancelPowerOnJob();
1653         power = false;
1654         updateChannelState(CHANNEL_POWER);
1655         updateChannelState(CHANNEL_MAIN_POWER);
1656         updateChannelState(CHANNEL_SOURCE);
1657         updateChannelState(CHANNEL_MAIN_SOURCE);
1658         updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1659         updateChannelState(CHANNEL_DSP);
1660         updateChannelState(CHANNEL_MAIN_DSP);
1661         updateChannelState(CHANNEL_VOLUME);
1662         updateChannelState(CHANNEL_MAIN_VOLUME);
1663         updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1664         updateChannelState(CHANNEL_MUTE);
1665         updateChannelState(CHANNEL_MAIN_MUTE);
1666         updateChannelState(CHANNEL_BASS);
1667         updateChannelState(CHANNEL_MAIN_BASS);
1668         updateChannelState(CHANNEL_TREBLE);
1669         updateChannelState(CHANNEL_MAIN_TREBLE);
1670         updateChannelState(CHANNEL_PLAY_CONTROL);
1671         updateChannelState(CHANNEL_TRACK);
1672         updateChannelState(CHANNEL_FREQUENCY);
1673         updateChannelState(CHANNEL_BRIGHTNESS);
1674         updateChannelState(CHANNEL_TCBYPASS);
1675         updateChannelState(CHANNEL_BALANCE);
1676         updateChannelState(CHANNEL_SPEAKER_A);
1677         updateChannelState(CHANNEL_SPEAKER_B);
1678     }
1679
1680     /**
1681      * Handle the received information that zone 2 power is ON
1682      */
1683     private void handlePowerOnZone2() {
1684         boolean prev = powerZone2;
1685         powerZone2 = true;
1686         updateChannelState(CHANNEL_ZONE2_POWER);
1687         if (!prev) {
1688             schedulePowerOnZone2Job();
1689         }
1690     }
1691
1692     /**
1693      * Handle the received information that zone 2 power is OFF
1694      */
1695     private void handlePowerOffZone2() {
1696         cancelPowerOnZone2Job();
1697         powerZone2 = false;
1698         updateChannelState(CHANNEL_ZONE2_POWER);
1699         updateChannelState(CHANNEL_ZONE2_SOURCE);
1700         updateChannelState(CHANNEL_ZONE2_VOLUME);
1701         updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1702         updateChannelState(CHANNEL_ZONE2_MUTE);
1703     }
1704
1705     /**
1706      * Handle the received information that zone 3 power is ON
1707      */
1708     private void handlePowerOnZone3() {
1709         boolean prev = powerZone3;
1710         powerZone3 = true;
1711         updateChannelState(CHANNEL_ZONE3_POWER);
1712         if (!prev) {
1713             schedulePowerOnZone3Job();
1714         }
1715     }
1716
1717     /**
1718      * Handle the received information that zone 3 power is OFF
1719      */
1720     private void handlePowerOffZone3() {
1721         cancelPowerOnZone3Job();
1722         powerZone3 = false;
1723         updateChannelState(CHANNEL_ZONE3_POWER);
1724         updateChannelState(CHANNEL_ZONE3_SOURCE);
1725         updateChannelState(CHANNEL_ZONE3_VOLUME);
1726         updateChannelState(CHANNEL_ZONE3_MUTE);
1727     }
1728
1729     /**
1730      * Handle the received information that zone 4 power is ON
1731      */
1732     private void handlePowerOnZone4() {
1733         boolean prev = powerZone4;
1734         powerZone4 = true;
1735         updateChannelState(CHANNEL_ZONE4_POWER);
1736         if (!prev) {
1737             schedulePowerOnZone4Job();
1738         }
1739     }
1740
1741     /**
1742      * Handle the received information that zone 4 power is OFF
1743      */
1744     private void handlePowerOffZone4() {
1745         cancelPowerOnZone4Job();
1746         powerZone4 = false;
1747         updateChannelState(CHANNEL_ZONE4_POWER);
1748         updateChannelState(CHANNEL_ZONE4_SOURCE);
1749         updateChannelState(CHANNEL_ZONE4_VOLUME);
1750         updateChannelState(CHANNEL_ZONE4_MUTE);
1751     }
1752
1753     /**
1754      * Schedule the job that will consider the device as OFF if no new event is received before its running
1755      *
1756      * @param switchOffAllZones true if all zones have to be considered as OFF
1757      */
1758     private void schedulePowerOffJob(boolean switchOffAllZones) {
1759         logger.debug("Schedule power OFF job");
1760         cancelPowerOffJob();
1761         powerOffJob = scheduler.schedule(() -> {
1762             logger.debug("Power OFF job");
1763             handlePowerOff();
1764             if (switchOffAllZones) {
1765                 handlePowerOffZone2();
1766                 handlePowerOffZone3();
1767                 handlePowerOffZone4();
1768             }
1769         }, 2000, TimeUnit.MILLISECONDS);
1770     }
1771
1772     /**
1773      * Cancel the job that will consider the device as OFF
1774      */
1775     private void cancelPowerOffJob() {
1776         ScheduledFuture<?> powerOffJob = this.powerOffJob;
1777         if (powerOffJob != null && !powerOffJob.isCancelled()) {
1778             powerOffJob.cancel(true);
1779             this.powerOffJob = null;
1780         }
1781     }
1782
1783     /**
1784      * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1785      */
1786     private void schedulePowerOnJob() {
1787         logger.debug("Schedule power ON job");
1788         cancelPowerOnJob();
1789         powerOnJob = scheduler.schedule(() -> {
1790             synchronized (sequenceLock) {
1791                 logger.debug("Power ON job");
1792                 try {
1793                     switch (protocol) {
1794                         case HEX:
1795                             if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1796                                 sendCommand(getVolumeDownCommand());
1797                                 Thread.sleep(100);
1798                                 sendCommand(getVolumeUpCommand());
1799                                 Thread.sleep(100);
1800                             }
1801                             if (model.getNbAdditionalZones() >= 1) {
1802                                 if (currentZone != 1
1803                                         && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1804                                     selectZone(1, model.getZoneSelectCmd());
1805                                 } else if (!selectingRecord) {
1806                                     sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1807                                     Thread.sleep(100);
1808                                 }
1809                             } else {
1810                                 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1811                                 Thread.sleep(100);
1812                             }
1813                             if (model.hasToneControl()) {
1814                                 if (model == RotelModel.RSX1065) {
1815                                     // No tone control select command
1816                                     sendCommand(RotelCommand.TREBLE_DOWN);
1817                                     Thread.sleep(100);
1818                                     sendCommand(RotelCommand.TREBLE_UP);
1819                                     Thread.sleep(100);
1820                                     sendCommand(RotelCommand.BASS_DOWN);
1821                                     Thread.sleep(100);
1822                                     sendCommand(RotelCommand.BASS_UP);
1823                                     Thread.sleep(100);
1824                                 } else {
1825                                     selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1826                                 }
1827                             }
1828                             break;
1829                         case ASCII_V1:
1830                             if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1831                                     && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1832                                 sendCommand(RotelCommand.UPDATE_AUTO);
1833                                 Thread.sleep(SLEEP_INTV);
1834                             }
1835                             if (model.hasSourceControl()) {
1836                                 sendCommand(RotelCommand.SOURCE);
1837                                 Thread.sleep(SLEEP_INTV);
1838                             }
1839                             if (model.hasVolumeControl() || model.hasToneControl()) {
1840                                 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1841                                         && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1842                                     sendCommand(RotelCommand.VOLUME_GET_MIN);
1843                                     Thread.sleep(SLEEP_INTV);
1844                                     sendCommand(RotelCommand.VOLUME_GET_MAX);
1845                                     Thread.sleep(SLEEP_INTV);
1846                                 }
1847                                 if (model.hasToneControl()) {
1848                                     sendCommand(RotelCommand.TONE_MAX);
1849                                     Thread.sleep(SLEEP_INTV);
1850                                 }
1851                                 // Wait enough to be sure to get the min/max values requested just before
1852                                 Thread.sleep(250);
1853                                 if (model.hasVolumeControl()) {
1854                                     sendCommand(RotelCommand.VOLUME_GET);
1855                                     Thread.sleep(SLEEP_INTV);
1856                                     if (model != RotelModel.RA11 && model != RotelModel.RA12
1857                                             && model != RotelModel.RCX1500) {
1858                                         sendCommand(RotelCommand.MUTE);
1859                                         Thread.sleep(SLEEP_INTV);
1860                                     }
1861                                 }
1862                                 if (model.hasToneControl()) {
1863                                     sendCommand(RotelCommand.BASS);
1864                                     Thread.sleep(SLEEP_INTV);
1865                                     sendCommand(RotelCommand.TREBLE);
1866                                     Thread.sleep(SLEEP_INTV);
1867                                     sendCommand(RotelCommand.TONE_CONTROLS);
1868                                     Thread.sleep(SLEEP_INTV);
1869                                 }
1870                             }
1871                             if (model.hasBalanceControl()) {
1872                                 sendCommand(RotelCommand.BALANCE);
1873                                 Thread.sleep(SLEEP_INTV);
1874                             }
1875                             if (model.hasPlayControl()) {
1876                                 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1877                                         && (model != RotelModel.RCX1500 || !source.getName().equals("CD"))) {
1878                                     sendCommand(RotelCommand.PLAY_STATUS);
1879                                     Thread.sleep(SLEEP_INTV);
1880                                 } else {
1881                                     sendCommand(RotelCommand.CD_PLAY_STATUS);
1882                                     Thread.sleep(SLEEP_INTV);
1883                                 }
1884                             }
1885                             if (model.hasDspControl()) {
1886                                 sendCommand(RotelCommand.DSP_MODE);
1887                                 Thread.sleep(SLEEP_INTV);
1888                             }
1889                             if (model.canGetFrequency()) {
1890                                 sendCommand(RotelCommand.FREQUENCY);
1891                                 Thread.sleep(SLEEP_INTV);
1892                             }
1893                             if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1894                                 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1895                                 Thread.sleep(SLEEP_INTV);
1896                             }
1897                             if (model.hasSpeakerGroups()) {
1898                                 sendCommand(RotelCommand.SPEAKER);
1899                                 Thread.sleep(SLEEP_INTV);
1900                             }
1901                             break;
1902                         case ASCII_V2:
1903                             sendCommand(RotelCommand.UPDATE_AUTO);
1904                             Thread.sleep(SLEEP_INTV);
1905                             if (model.hasSourceControl()) {
1906                                 sendCommand(RotelCommand.SOURCE);
1907                                 Thread.sleep(SLEEP_INTV);
1908                             }
1909                             if (model.hasVolumeControl()) {
1910                                 sendCommand(RotelCommand.VOLUME_GET);
1911                                 Thread.sleep(SLEEP_INTV);
1912                                 sendCommand(RotelCommand.MUTE);
1913                                 Thread.sleep(SLEEP_INTV);
1914                             }
1915                             if (model.hasToneControl()) {
1916                                 sendCommand(RotelCommand.BASS);
1917                                 Thread.sleep(SLEEP_INTV);
1918                                 sendCommand(RotelCommand.TREBLE);
1919                                 Thread.sleep(SLEEP_INTV);
1920                                 sendCommand(RotelCommand.TCBYPASS);
1921                                 Thread.sleep(SLEEP_INTV);
1922                             }
1923                             if (model.hasBalanceControl()) {
1924                                 sendCommand(RotelCommand.BALANCE);
1925                                 Thread.sleep(SLEEP_INTV);
1926                             }
1927                             if (model.hasPlayControl()) {
1928                                 sendCommand(RotelCommand.PLAY_STATUS);
1929                                 Thread.sleep(SLEEP_INTV);
1930                                 if (source.getName().equals("CD") && !model.hasSourceControl()) {
1931                                     sendCommand(RotelCommand.TRACK);
1932                                     Thread.sleep(SLEEP_INTV);
1933                                 }
1934                             }
1935                             if (model.hasDspControl()) {
1936                                 sendCommand(RotelCommand.DSP_MODE);
1937                                 Thread.sleep(SLEEP_INTV);
1938                             }
1939                             if (model.canGetFrequency()) {
1940                                 sendCommand(RotelCommand.FREQUENCY);
1941                                 Thread.sleep(SLEEP_INTV);
1942                             }
1943                             if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1944                                 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1945                                 Thread.sleep(SLEEP_INTV);
1946                             }
1947                             if (model.hasSpeakerGroups()) {
1948                                 sendCommand(RotelCommand.SPEAKER);
1949                                 Thread.sleep(SLEEP_INTV);
1950                             }
1951                             break;
1952                     }
1953                 } catch (RotelException e) {
1954                     logger.debug("Init sequence failed: {}", e.getMessage());
1955                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1956                             "@text/offline.comm-error-init-sequence");
1957                     closeConnection();
1958                 } catch (InterruptedException e) {
1959                     logger.debug("Init sequence interrupted: {}", e.getMessage());
1960                     Thread.currentThread().interrupt();
1961                 }
1962             }
1963         }, 2500, TimeUnit.MILLISECONDS);
1964     }
1965
1966     /**
1967      * Cancel the job scheduled when the device power (main zone) switched ON
1968      */
1969     private void cancelPowerOnJob() {
1970         ScheduledFuture<?> powerOnJob = this.powerOnJob;
1971         if (powerOnJob != null && !powerOnJob.isCancelled()) {
1972             powerOnJob.cancel(true);
1973             this.powerOnJob = null;
1974         }
1975     }
1976
1977     /**
1978      * Schedule the job to run with a few seconds delay when the zone 2 power switched ON
1979      */
1980     private void schedulePowerOnZone2Job() {
1981         logger.debug("Schedule power ON zone 2 job");
1982         cancelPowerOnZone2Job();
1983         powerOnZone2Job = scheduler.schedule(() -> {
1984             synchronized (sequenceLock) {
1985                 logger.debug("Power ON zone 2 job");
1986                 try {
1987                     if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1) {
1988                         selectZone(2, model.getZoneSelectCmd());
1989                         sendCommand(
1990                                 model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
1991                         Thread.sleep(100);
1992                         sendCommand(model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP : RotelCommand.VOLUME_UP);
1993                         Thread.sleep(100);
1994                     }
1995                 } catch (RotelException e) {
1996                     logger.debug("Init sequence zone 2 failed: {}", e.getMessage());
1997                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1998                             "@text/offline.comm-error-init-sequence-zone [\"2\"]");
1999                     closeConnection();
2000                 } catch (InterruptedException e) {
2001                     logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage());
2002                     Thread.currentThread().interrupt();
2003                 }
2004             }
2005         }, 2500, TimeUnit.MILLISECONDS);
2006     }
2007
2008     /**
2009      * Cancel the job scheduled when the zone 2 power switched ON
2010      */
2011     private void cancelPowerOnZone2Job() {
2012         ScheduledFuture<?> powerOnZone2Job = this.powerOnZone2Job;
2013         if (powerOnZone2Job != null && !powerOnZone2Job.isCancelled()) {
2014             powerOnZone2Job.cancel(true);
2015             this.powerOnZone2Job = null;
2016         }
2017     }
2018
2019     /**
2020      * Schedule the job to run with a few seconds delay when the zone 3 power switched ON
2021      */
2022     private void schedulePowerOnZone3Job() {
2023         logger.debug("Schedule power ON zone 3 job");
2024         cancelPowerOnZone3Job();
2025         powerOnZone3Job = scheduler.schedule(() -> {
2026             synchronized (sequenceLock) {
2027                 logger.debug("Power ON zone 3 job");
2028                 try {
2029                     if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 2) {
2030                         selectZone(3, model.getZoneSelectCmd());
2031                         sendCommand(
2032                                 model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
2033                         Thread.sleep(100);
2034                         sendCommand(model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP : RotelCommand.VOLUME_UP);
2035                         Thread.sleep(100);
2036                     }
2037                 } catch (RotelException e) {
2038                     logger.debug("Init sequence zone 3 failed: {}", e.getMessage());
2039                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2040                             "@text/offline.comm-error-init-sequence-zone [\"3\"]");
2041                     closeConnection();
2042                 } catch (InterruptedException e) {
2043                     logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage());
2044                     Thread.currentThread().interrupt();
2045                 }
2046             }
2047         }, 2500, TimeUnit.MILLISECONDS);
2048     }
2049
2050     /**
2051      * Cancel the job scheduled when the zone 3 power switched ON
2052      */
2053     private void cancelPowerOnZone3Job() {
2054         ScheduledFuture<?> powerOnZone3Job = this.powerOnZone3Job;
2055         if (powerOnZone3Job != null && !powerOnZone3Job.isCancelled()) {
2056             powerOnZone3Job.cancel(true);
2057             this.powerOnZone3Job = null;
2058         }
2059     }
2060
2061     /**
2062      * Schedule the job to run with a few seconds delay when the zone 4 power switched ON
2063      */
2064     private void schedulePowerOnZone4Job() {
2065         logger.debug("Schedule power ON zone 4 job");
2066         cancelPowerOnZone4Job();
2067         powerOnZone4Job = scheduler.schedule(() -> {
2068             synchronized (sequenceLock) {
2069                 logger.debug("Power ON zone 4 job");
2070                 try {
2071                     if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 3) {
2072                         selectZone(4, model.getZoneSelectCmd());
2073                         sendCommand(
2074                                 model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
2075                         Thread.sleep(100);
2076                         sendCommand(model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP : RotelCommand.VOLUME_UP);
2077                         Thread.sleep(100);
2078                     }
2079                 } catch (RotelException e) {
2080                     logger.debug("Init sequence zone 4 failed: {}", e.getMessage());
2081                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2082                             "@text/offline.comm-error-init-sequence-zone [\"4\"]");
2083                     closeConnection();
2084                 } catch (InterruptedException e) {
2085                     logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage());
2086                     Thread.currentThread().interrupt();
2087                 }
2088             }
2089         }, 2500, TimeUnit.MILLISECONDS);
2090     }
2091
2092     /**
2093      * Cancel the job scheduled when the zone 4 power switched ON
2094      */
2095     private void cancelPowerOnZone4Job() {
2096         ScheduledFuture<?> powerOnZone4Job = this.powerOnZone4Job;
2097         if (powerOnZone4Job != null && !powerOnZone4Job.isCancelled()) {
2098             powerOnZone4Job.cancel(true);
2099             this.powerOnZone4Job = null;
2100         }
2101     }
2102
2103     /**
2104      * Schedule the reconnection job
2105      */
2106     private void scheduleReconnectJob() {
2107         logger.debug("Schedule reconnect job");
2108         cancelReconnectJob();
2109         reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2110             if (!connector.isConnected()) {
2111                 logger.debug("Trying to reconnect...");
2112                 closeConnection();
2113                 power = null;
2114                 String error = null;
2115                 if (openConnection()) {
2116                     synchronized (sequenceLock) {
2117                         schedulePowerOffJob(true);
2118                         try {
2119                             sendCommand(model.getPowerStateCmd());
2120                         } catch (RotelException e) {
2121                             error = "@text/offline.comm-error-first-command-after-reconnection";
2122                             logger.debug("First command after connection failed", e);
2123                             cancelPowerOffJob();
2124                             closeConnection();
2125                         }
2126                     }
2127                 } else {
2128                     error = "@text/offline.comm-error-reconnection";
2129                 }
2130                 if (error != null) {
2131                     handlePowerOff();
2132                     handlePowerOffZone2();
2133                     handlePowerOffZone3();
2134                     handlePowerOffZone4();
2135                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2136                 } else {
2137                     updateStatus(ThingStatus.ONLINE);
2138                 }
2139             }
2140         }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2141     }
2142
2143     /**
2144      * Cancel the reconnection job
2145      */
2146     private void cancelReconnectJob() {
2147         ScheduledFuture<?> reconnectJob = this.reconnectJob;
2148         if (reconnectJob != null && !reconnectJob.isCancelled()) {
2149             reconnectJob.cancel(true);
2150             this.reconnectJob = null;
2151         }
2152     }
2153
2154     /**
2155      * Update the state of a channel
2156      *
2157      * @param channel the channel
2158      */
2159     private void updateChannelState(String channel) {
2160         if (!isLinked(channel)) {
2161             return;
2162         }
2163         State state = UnDefType.UNDEF;
2164         switch (channel) {
2165             case CHANNEL_POWER:
2166             case CHANNEL_MAIN_POWER:
2167                 Boolean po = power;
2168                 if (po != null) {
2169                     state = OnOffType.from(po.booleanValue());
2170                 }
2171                 break;
2172             case CHANNEL_ZONE2_POWER:
2173                 state = OnOffType.from(powerZone2);
2174                 break;
2175             case CHANNEL_ZONE3_POWER:
2176                 state = OnOffType.from(powerZone3);
2177                 break;
2178             case CHANNEL_ZONE4_POWER:
2179                 state = OnOffType.from(powerZone4);
2180                 break;
2181             case CHANNEL_SOURCE:
2182             case CHANNEL_MAIN_SOURCE:
2183                 if (isPowerOn()) {
2184                     state = new StringType(source.getName());
2185                 }
2186                 break;
2187             case CHANNEL_MAIN_RECORD_SOURCE:
2188                 RotelSource recordSource = this.recordSource;
2189                 if (isPowerOn() && recordSource != null) {
2190                     state = new StringType(recordSource.getName());
2191                 }
2192                 break;
2193             case CHANNEL_ZONE2_SOURCE:
2194                 RotelSource sourceZone2 = this.sourceZone2;
2195                 if (powerZone2 && sourceZone2 != null) {
2196                     state = new StringType(sourceZone2.getName());
2197                 }
2198                 break;
2199             case CHANNEL_ZONE3_SOURCE:
2200                 RotelSource sourceZone3 = this.sourceZone3;
2201                 if (powerZone3 && sourceZone3 != null) {
2202                     state = new StringType(sourceZone3.getName());
2203                 }
2204                 break;
2205             case CHANNEL_ZONE4_SOURCE:
2206                 RotelSource sourceZone4 = this.sourceZone4;
2207                 if (powerZone4 && sourceZone4 != null) {
2208                     state = new StringType(sourceZone4.getName());
2209                 }
2210                 break;
2211             case CHANNEL_DSP:
2212             case CHANNEL_MAIN_DSP:
2213                 if (isPowerOn()) {
2214                     state = new StringType(dsp.getName());
2215                 }
2216                 break;
2217             case CHANNEL_VOLUME:
2218             case CHANNEL_MAIN_VOLUME:
2219                 if (isPowerOn()) {
2220                     long volumePct = Math
2221                             .round((double) (volume - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2222                     state = new PercentType(BigDecimal.valueOf(volumePct));
2223                 }
2224                 break;
2225             case CHANNEL_MAIN_VOLUME_UP_DOWN:
2226                 if (isPowerOn()) {
2227                     state = new DecimalType(volume);
2228                 }
2229                 break;
2230             case CHANNEL_ZONE2_VOLUME:
2231                 if (powerZone2 && !fixedVolumeZone2) {
2232                     long volumePct = Math
2233                             .round((double) (volumeZone2 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2234                     state = new PercentType(BigDecimal.valueOf(volumePct));
2235                 }
2236                 break;
2237             case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2238                 if (powerZone2 && !fixedVolumeZone2) {
2239                     state = new DecimalType(volumeZone2);
2240                 }
2241                 break;
2242             case CHANNEL_ZONE3_VOLUME:
2243                 if (powerZone3 && !fixedVolumeZone3) {
2244                     long volumePct = Math
2245                             .round((double) (volumeZone3 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2246                     state = new PercentType(BigDecimal.valueOf(volumePct));
2247                 }
2248                 break;
2249             case CHANNEL_ZONE4_VOLUME:
2250                 if (powerZone4 && !fixedVolumeZone4) {
2251                     long volumePct = Math
2252                             .round((double) (volumeZone4 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2253                     state = new PercentType(BigDecimal.valueOf(volumePct));
2254                 }
2255                 break;
2256             case CHANNEL_MUTE:
2257             case CHANNEL_MAIN_MUTE:
2258                 if (isPowerOn()) {
2259                     state = OnOffType.from(mute);
2260                 }
2261                 break;
2262             case CHANNEL_ZONE2_MUTE:
2263                 if (powerZone2) {
2264                     state = OnOffType.from(muteZone2);
2265                 }
2266                 break;
2267             case CHANNEL_ZONE3_MUTE:
2268                 if (powerZone3) {
2269                     state = OnOffType.from(muteZone3);
2270                 }
2271                 break;
2272             case CHANNEL_ZONE4_MUTE:
2273                 if (powerZone4) {
2274                     state = OnOffType.from(muteZone4);
2275                 }
2276                 break;
2277             case CHANNEL_BASS:
2278             case CHANNEL_MAIN_BASS:
2279                 if (isPowerOn()) {
2280                     state = new DecimalType(bass);
2281                 }
2282                 break;
2283             case CHANNEL_TREBLE:
2284             case CHANNEL_MAIN_TREBLE:
2285                 if (isPowerOn()) {
2286                     state = new DecimalType(treble);
2287                 }
2288                 break;
2289             case CHANNEL_TRACK:
2290                 if (track > 0 && isPowerOn()) {
2291                     state = new DecimalType(track);
2292                 }
2293                 break;
2294             case CHANNEL_PLAY_CONTROL:
2295                 if (isPowerOn()) {
2296                     switch (playStatus) {
2297                         case PLAYING:
2298                             state = PlayPauseType.PLAY;
2299                             break;
2300                         case PAUSED:
2301                         case STOPPED:
2302                             state = PlayPauseType.PAUSE;
2303                             break;
2304                     }
2305                 }
2306                 break;
2307             case CHANNEL_FREQUENCY:
2308                 if (frequency > 0.0 && isPowerOn()) {
2309                     state = new DecimalType(frequency);
2310                 }
2311                 break;
2312             case CHANNEL_LINE1:
2313                 state = new StringType(frontPanelLine1);
2314                 break;
2315             case CHANNEL_LINE2:
2316                 state = new StringType(frontPanelLine2);
2317                 break;
2318             case CHANNEL_BRIGHTNESS:
2319                 if (isPowerOn() && model.hasDimmerControl()) {
2320                     long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2321                             / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2322                     state = new PercentType(BigDecimal.valueOf(dimmerPct));
2323                 }
2324                 break;
2325             case CHANNEL_TCBYPASS:
2326                 if (isPowerOn()) {
2327                     state = OnOffType.from(tcbypass);
2328                 }
2329                 break;
2330             case CHANNEL_BALANCE:
2331                 if (isPowerOn()) {
2332                     state = new DecimalType(balance);
2333                 }
2334                 break;
2335             case CHANNEL_SPEAKER_A:
2336                 if (isPowerOn()) {
2337                     state = OnOffType.from(speakera);
2338                 }
2339                 break;
2340             case CHANNEL_SPEAKER_B:
2341                 if (isPowerOn()) {
2342                     state = OnOffType.from(speakerb);
2343                 }
2344                 break;
2345             default:
2346                 break;
2347         }
2348         updateState(channel, state);
2349     }
2350
2351     /**
2352      * Inform about the main zone power state
2353      *
2354      * @return true if main zone power state is known and known as ON
2355      */
2356     private boolean isPowerOn() {
2357         Boolean power = this.power;
2358         return power != null && power.booleanValue();
2359     }
2360
2361     /**
2362      * Get the command to be used for main zone POWER ON
2363      *
2364      * @return the command
2365      */
2366     private RotelCommand getPowerOnCommand() {
2367         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2368     }
2369
2370     /**
2371      * Get the command to be used for main zone POWER OFF
2372      *
2373      * @return the command
2374      */
2375     private RotelCommand getPowerOffCommand() {
2376         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2377     }
2378
2379     /**
2380      * Get the command to be used for main zone VOLUME UP
2381      *
2382      * @return the command
2383      */
2384     private RotelCommand getVolumeUpCommand() {
2385         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2386     }
2387
2388     /**
2389      * Get the command to be used for main zone VOLUME DOWN
2390      *
2391      * @return the command
2392      */
2393     private RotelCommand getVolumeDownCommand() {
2394         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN : RotelCommand.VOLUME_DOWN;
2395     }
2396
2397     /**
2398      * Get the command to be used for main zone MUTE ON
2399      *
2400      * @return the command
2401      */
2402     private RotelCommand getMuteOnCommand() {
2403         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2404     }
2405
2406     /**
2407      * Get the command to be used for main zone MUTE OFF
2408      *
2409      * @return the command
2410      */
2411     private RotelCommand getMuteOffCommand() {
2412         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2413     }
2414
2415     /**
2416      * Get the command to be used for main zone MUTE TOGGLE
2417      *
2418      * @return the command
2419      */
2420     private RotelCommand getMuteToggleCommand() {
2421         return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE : RotelCommand.MUTE_TOGGLE;
2422     }
2423
2424     private void sendCommand(RotelCommand cmd) throws RotelException {
2425         sendCommand(cmd, null);
2426     }
2427
2428     /**
2429      * Request the Rotel device to execute a command
2430      *
2431      * @param cmd the command to execute
2432      * @param value the integer value to consider for volume, bass or treble adjustment
2433      *
2434      * @throws RotelException - In case of any problem
2435      */
2436     private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2437         byte[] message;
2438         try {
2439             message = protocolHandler.buildCommandMessage(cmd, value);
2440         } catch (RotelException e) {
2441             // Command not supported
2442             logger.debug("sendCommand: {}", e.getMessage());
2443             return;
2444         }
2445         connector.writeOutput(cmd.getName(), message);
2446
2447         if (connector instanceof RotelSimuConnector) {
2448             if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2449                     || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2450                     || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2451                 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);
2452             }
2453         }
2454     }
2455 }