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