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