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