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