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