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