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