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