]> git.basschouten.com Git - openhab-addons.git/blob
d2fb766920bbf7fd7afb9fbd6b9d0034a383a138
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.rotel.internal.handler;
14
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.EventObject;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelBindingConstants;
29 import org.openhab.binding.rotel.internal.RotelException;
30 import org.openhab.binding.rotel.internal.RotelModel;
31 import org.openhab.binding.rotel.internal.RotelPlayStatus;
32 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
33 import org.openhab.binding.rotel.internal.communication.RotelCommand;
34 import org.openhab.binding.rotel.internal.communication.RotelConnector;
35 import org.openhab.binding.rotel.internal.communication.RotelDsp;
36 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelMessageEvent;
38 import org.openhab.binding.rotel.internal.communication.RotelMessageEventListener;
39 import org.openhab.binding.rotel.internal.communication.RotelProtocol;
40 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
41 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
42 import org.openhab.binding.rotel.internal.communication.RotelSource;
43 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
44 import org.openhab.core.io.transport.serial.SerialPortManager;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.IncreaseDecreaseType;
47 import org.openhab.core.library.types.NextPreviousType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.library.types.PlayPauseType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.openhab.core.types.StateOption;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 /**
66  * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
67  *
68  * @author Laurent Garnier - Initial contribution
69  */
70 @NonNullByDefault
71 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
72
73     private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
74
75     private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
76     private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
77     private static final boolean USE_SIMULATED_DEVICE = false;
78
79     private @Nullable ScheduledFuture<?> reconnectJob;
80     private @Nullable ScheduledFuture<?> powerOnJob;
81     private @Nullable ScheduledFuture<?> powerOffJob;
82     private @Nullable ScheduledFuture<?> powerOnZone2Job;
83     private @Nullable ScheduledFuture<?> powerOnZone3Job;
84     private @Nullable ScheduledFuture<?> powerOnZone4Job;
85
86     private RotelStateDescriptionOptionProvider stateDescriptionProvider;
87     private SerialPortManager serialPortManager;
88
89     private RotelConnector connector = new RotelSimuConnector(DEFAULT_MODEL, RotelProtocol.HEX, new HashMap<>(),
90             "OH-binding-rotel");
91
92     private int minVolume;
93     private int maxVolume;
94     private int minToneLevel;
95     private int maxToneLevel;
96
97     private int currentZone = 1;
98     private boolean selectingRecord;
99     private @Nullable Boolean power;
100     private boolean powerZone2;
101     private boolean powerZone3;
102     private boolean powerZone4;
103     private RotelSource source = RotelSource.CAT0_CD;
104     private @Nullable RotelSource recordSource;
105     private @Nullable RotelSource sourceZone2;
106     private @Nullable RotelSource sourceZone3;
107     private @Nullable RotelSource sourceZone4;
108     private RotelDsp dsp = RotelDsp.CAT1_NONE;
109     private int volume;
110     private boolean mute;
111     private boolean fixedVolumeZone2;
112     private int volumeZone2;
113     private boolean muteZone2;
114     private boolean fixedVolumeZone3;
115     private int volumeZone3;
116     private boolean muteZone3;
117     private boolean fixedVolumeZone4;
118     private int volumeZone4;
119     private boolean muteZone4;
120     private int bass;
121     private int treble;
122     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
123     private int track;
124     private double frequency;
125     private String frontPanelLine1 = "";
126     private String frontPanelLine2 = "";
127     private int brightness;
128
129     private Object sequenceLock = new Object();
130
131     /**
132      * Constructor
133      */
134     public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
135             SerialPortManager serialPortManager) {
136         super(thing);
137         this.stateDescriptionProvider = stateDescriptionProvider;
138         this.serialPortManager = serialPortManager;
139     }
140
141     @Override
142     public void initialize() {
143         logger.debug("Start initializing handler for thing {}", getThing().getUID());
144
145         RotelModel rotelModel;
146         switch (getThing().getThingTypeUID().getId()) {
147             case THING_TYPE_ID_RSP1066:
148                 rotelModel = RotelModel.RSP1066;
149                 break;
150             case THING_TYPE_ID_RSP1068:
151                 rotelModel = RotelModel.RSP1068;
152                 break;
153             case THING_TYPE_ID_RSP1069:
154                 rotelModel = RotelModel.RSP1069;
155                 break;
156             case THING_TYPE_ID_RSP1098:
157                 rotelModel = RotelModel.RSP1098;
158                 break;
159             case THING_TYPE_ID_RSP1570:
160                 rotelModel = RotelModel.RSP1570;
161                 break;
162             case THING_TYPE_ID_RSP1572:
163                 rotelModel = RotelModel.RSP1572;
164                 break;
165             case THING_TYPE_ID_RSX1055:
166                 rotelModel = RotelModel.RSX1055;
167                 break;
168             case THING_TYPE_ID_RSX1056:
169                 rotelModel = RotelModel.RSX1056;
170                 break;
171             case THING_TYPE_ID_RSX1057:
172                 rotelModel = RotelModel.RSX1057;
173                 break;
174             case THING_TYPE_ID_RSX1058:
175                 rotelModel = RotelModel.RSX1058;
176                 break;
177             case THING_TYPE_ID_RSX1065:
178                 rotelModel = RotelModel.RSX1065;
179                 break;
180             case THING_TYPE_ID_RSX1067:
181                 rotelModel = RotelModel.RSX1067;
182                 break;
183             case THING_TYPE_ID_RSX1550:
184                 rotelModel = RotelModel.RSX1550;
185                 break;
186             case THING_TYPE_ID_RSX1560:
187                 rotelModel = RotelModel.RSX1560;
188                 break;
189             case THING_TYPE_ID_RSX1562:
190                 rotelModel = RotelModel.RSX1562;
191                 break;
192             case THING_TYPE_ID_A11:
193                 rotelModel = RotelModel.A11;
194                 break;
195             case THING_TYPE_ID_A12:
196                 rotelModel = RotelModel.A12;
197                 break;
198             case THING_TYPE_ID_A14:
199                 rotelModel = RotelModel.A14;
200                 break;
201             case THING_TYPE_ID_CD11:
202                 rotelModel = RotelModel.CD11;
203                 break;
204             case THING_TYPE_ID_CD14:
205                 rotelModel = RotelModel.CD14;
206                 break;
207             case THING_TYPE_ID_RA11:
208                 rotelModel = RotelModel.RA11;
209                 break;
210             case THING_TYPE_ID_RA12:
211                 rotelModel = RotelModel.RA12;
212                 break;
213             case THING_TYPE_ID_RA1570:
214                 rotelModel = RotelModel.RA1570;
215                 break;
216             case THING_TYPE_ID_RA1572:
217                 rotelModel = RotelModel.RA1572;
218                 break;
219             case THING_TYPE_ID_RA1592:
220                 rotelModel = RotelModel.RA1592;
221                 break;
222             case THING_TYPE_ID_RAP1580:
223                 rotelModel = RotelModel.RAP1580;
224                 break;
225             case THING_TYPE_ID_RC1570:
226                 rotelModel = RotelModel.RC1570;
227                 break;
228             case THING_TYPE_ID_RC1572:
229                 rotelModel = RotelModel.RC1572;
230                 break;
231             case THING_TYPE_ID_RC1590:
232                 rotelModel = RotelModel.RC1590;
233                 break;
234             case THING_TYPE_ID_RCD1570:
235                 rotelModel = RotelModel.RCD1570;
236                 break;
237             case THING_TYPE_ID_RCD1572:
238                 rotelModel = RotelModel.RCD1572;
239                 break;
240             case THING_TYPE_ID_RCX1500:
241                 rotelModel = RotelModel.RCX1500;
242                 break;
243             case THING_TYPE_ID_RDD1580:
244                 rotelModel = RotelModel.RDD1580;
245                 break;
246             case THING_TYPE_ID_RDG1520:
247             case THING_TYPE_ID_RT09:
248                 rotelModel = RotelModel.RDG1520;
249                 break;
250             case THING_TYPE_ID_RSP1576:
251                 rotelModel = RotelModel.RSP1576;
252                 break;
253             case THING_TYPE_ID_RSP1582:
254                 rotelModel = RotelModel.RSP1582;
255                 break;
256             case THING_TYPE_ID_RT11:
257                 rotelModel = RotelModel.RT11;
258                 break;
259             case THING_TYPE_ID_RT1570:
260                 rotelModel = RotelModel.RT1570;
261                 break;
262             case THING_TYPE_ID_T11:
263                 rotelModel = RotelModel.T11;
264                 break;
265             case THING_TYPE_ID_T14:
266                 rotelModel = RotelModel.T14;
267                 break;
268             default:
269                 rotelModel = DEFAULT_MODEL;
270                 break;
271         }
272
273         RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
274
275         RotelProtocol rotelProtocol = RotelProtocol.HEX;
276         if (config.protocol != null && !config.protocol.isEmpty()) {
277             try {
278                 rotelProtocol = RotelProtocol.getFromName(config.protocol);
279             } catch (RotelException e) {
280             }
281         } else {
282             Map<String, String> properties = editProperties();
283             String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
284             if (property != null && !property.isEmpty()) {
285                 try {
286                     rotelProtocol = RotelProtocol.getFromName(property);
287                 } catch (RotelException e) {
288                 }
289             }
290         }
291         logger.debug("rotelProtocol {}", rotelProtocol.getName());
292
293         Map<RotelSource, String> sourcesLabels = new HashMap<>();
294
295         String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
296
297         connector = new RotelSimuConnector(rotelModel, rotelProtocol, sourcesLabels, readerThreadName);
298
299         if (rotelModel.hasVolumeControl()) {
300             maxVolume = rotelModel.getVolumeMax();
301             if (!rotelModel.hasDirectVolumeControl()) {
302                 logger.info(
303                         "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
304                         minVolume, maxVolume);
305             }
306         }
307         if (rotelModel.hasToneControl()) {
308             maxToneLevel = rotelModel.getToneLevelMax();
309             minToneLevel = -maxToneLevel;
310             logger.info(
311                     "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
312                     minToneLevel, maxToneLevel);
313         }
314
315         // Check configuration settings
316         String configError = null;
317         if ((config.serialPort == null || config.serialPort.isEmpty())
318                 && (config.host == null || config.host.isEmpty())) {
319             configError = "undefined serialPort and host configuration settings; please set one of them";
320         } else if (config.host == null || config.host.isEmpty()) {
321             if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
322                 configError = "use host and port configuration settings for a serial over IP connection";
323             }
324         } else {
325             if (config.port == null) {
326                 configError = "undefined port configuration setting";
327             } else if (config.port <= 0) {
328                 configError = "invalid port configuration setting";
329             }
330         }
331
332         if (configError != null) {
333             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
334         } else {
335             for (RotelSource src : rotelModel.getSources()) {
336                 // Consider custom input labels
337                 String label = null;
338                 switch (src.getName()) {
339                     case "CD":
340                         label = config.inputLabelCd;
341                         break;
342                     case "TUNER":
343                         label = config.inputLabelTuner;
344                         break;
345                     case "TAPE":
346                         label = config.inputLabelTape;
347                         break;
348                     case "PHONO":
349                         label = config.inputLabelPhono;
350                         break;
351                     case "VIDEO1":
352                         label = config.inputLabelVideo1;
353                         break;
354                     case "VIDEO2":
355                         label = config.inputLabelVideo2;
356                         break;
357                     case "VIDEO3":
358                         label = config.inputLabelVideo3;
359                         break;
360                     case "VIDEO4":
361                         label = config.inputLabelVideo4;
362                         break;
363                     case "VIDEO5":
364                         label = config.inputLabelVideo5;
365                         break;
366                     case "VIDEO6":
367                         label = config.inputLabelVideo6;
368                         break;
369                     case "USB":
370                         label = config.inputLabelUsb;
371                         break;
372                     case "MULTI":
373                         label = config.inputLabelMulti;
374                         break;
375                     default:
376                         break;
377                 }
378                 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
379             }
380
381             if (USE_SIMULATED_DEVICE) {
382                 connector = new RotelSimuConnector(rotelModel, rotelProtocol, sourcesLabels, readerThreadName);
383             } else if (config.serialPort != null) {
384                 connector = new RotelSerialConnector(serialPortManager, config.serialPort, rotelModel, rotelProtocol,
385                         sourcesLabels, readerThreadName);
386             } else {
387                 connector = new RotelIpConnector(config.host, config.port, rotelModel, rotelProtocol, sourcesLabels,
388                         readerThreadName);
389             }
390
391             if (rotelModel.hasSourceControl()) {
392                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
393                         getStateOptions(rotelModel.getSources(), sourcesLabels));
394                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
395                         getStateOptions(rotelModel.getSources(), sourcesLabels));
396                 stateDescriptionProvider.setStateOptions(
397                         new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
398                         getStateOptions(rotelModel.getRecordSources(), sourcesLabels));
399             }
400             if (rotelModel.hasZone2SourceControl()) {
401                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
402                         getStateOptions(rotelModel.getZone2Sources(), sourcesLabels));
403             }
404             if (rotelModel.hasZone3SourceControl()) {
405                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
406                         getStateOptions(rotelModel.getZone3Sources(), sourcesLabels));
407             }
408             if (rotelModel.hasZone4SourceControl()) {
409                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
410                         getStateOptions(rotelModel.getZone4Sources(), sourcesLabels));
411             }
412             if (rotelModel.hasDspControl()) {
413                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
414                         rotelModel.getDspStateOptions());
415                 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
416                         rotelModel.getDspStateOptions());
417             }
418
419             updateStatus(ThingStatus.UNKNOWN);
420
421             scheduleReconnectJob();
422         }
423
424         logger.debug("Finished initializing!");
425     }
426
427     @Override
428     public void dispose() {
429         logger.debug("Disposing handler for thing {}", getThing().getUID());
430         cancelPowerOffJob();
431         cancelPowerOnJob();
432         cancelPowerOnZone2Job();
433         cancelPowerOnZone3Job();
434         cancelPowerOnZone4Job();
435         cancelReconnectJob();
436         closeConnection();
437         super.dispose();
438     }
439
440     public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
441         List<StateOption> options = new ArrayList<>();
442         for (RotelSource item : list) {
443             String label = sourcesLabels.get(item);
444             options.add(new StateOption(item.getName(), label == null ? item.getLabel() : label));
445         }
446         return options;
447     }
448
449     @Override
450     public void handleCommand(ChannelUID channelUID, Command command) {
451         String channel = channelUID.getId();
452
453         if (getThing().getStatus() != ThingStatus.ONLINE) {
454             logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
455             return;
456         }
457
458         if (command instanceof RefreshType) {
459             updateChannelState(channel);
460             return;
461         }
462
463         if (!connector.isConnected()) {
464             logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
465             return;
466         }
467
468         RotelSource src;
469         RotelCommand cmd;
470         boolean success = true;
471         synchronized (sequenceLock) {
472             try {
473                 switch (channel) {
474                     case CHANNEL_POWER:
475                     case CHANNEL_MAIN_POWER:
476                         handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
477                         break;
478                     case CHANNEL_ZONE2_POWER:
479                         if (connector.getModel().hasZone2Commands()) {
480                             handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
481                         } else if (connector.getModel().getNbAdditionalZones() == 1) {
482                             if (isPowerOn() || powerZone2) {
483                                 selectZone(2, connector.getModel().getZoneSelectCmd());
484                             }
485                             connector.sendCommand(RotelCommand.ZONE_SELECT);
486                         } else {
487                             success = false;
488                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
489                         }
490                         break;
491                     case CHANNEL_ZONE3_POWER:
492                         if (connector.getModel().hasZone3Commands()) {
493                             handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
494                         } else {
495                             success = false;
496                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
497                         }
498                         break;
499                     case CHANNEL_ZONE4_POWER:
500                         if (connector.getModel().hasZone4Commands()) {
501                             handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
502                         } else {
503                             success = false;
504                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
505                         }
506                         break;
507                     case CHANNEL_SOURCE:
508                     case CHANNEL_MAIN_SOURCE:
509                         if (!isPowerOn()) {
510                             success = false;
511                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
512                         } else {
513                             src = connector.getModel().getSourceFromName(command.toString());
514                             cmd = connector.getModel().hasOtherThanPrimaryCommands() ? src.getMainZoneCommand()
515                                     : src.getCommand();
516                             if (cmd != null) {
517                                 connector.sendCommand(cmd);
518                             } else {
519                                 success = false;
520                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
521                                         channel);
522                             }
523                         }
524                         break;
525                     case CHANNEL_MAIN_RECORD_SOURCE:
526                         if (!isPowerOn()) {
527                             success = false;
528                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
529                         } else if (connector.getModel().hasOtherThanPrimaryCommands()) {
530                             src = connector.getModel().getSourceFromName(command.toString());
531                             cmd = src.getRecordCommand();
532                             if (cmd != null) {
533                                 connector.sendCommand(cmd);
534                             } else {
535                                 success = false;
536                                 logger.debug("Command {} from channel {} failed: undefined record source command",
537                                         command, channel);
538                             }
539                         } else {
540                             src = connector.getModel().getSourceFromName(command.toString());
541                             cmd = src.getCommand();
542                             if (cmd != null) {
543                                 connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
544                                 Thread.sleep(100);
545                                 connector.sendCommand(cmd);
546                             } else {
547                                 success = false;
548                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
549                                         channel);
550                             }
551                         }
552                         break;
553                     case CHANNEL_ZONE2_SOURCE:
554                         if (!powerZone2) {
555                             success = false;
556                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
557                         } else if (connector.getModel().hasZone2Commands()) {
558                             src = connector.getModel().getSourceFromName(command.toString());
559                             cmd = src.getZone2Command();
560                             if (cmd != null) {
561                                 connector.sendCommand(cmd);
562                             } else {
563                                 success = false;
564                                 logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
565                                         command, channel);
566                             }
567                         } else if (connector.getModel().getNbAdditionalZones() >= 1) {
568                             src = connector.getModel().getSourceFromName(command.toString());
569                             cmd = src.getCommand();
570                             if (cmd != null) {
571                                 selectZone(2, connector.getModel().getZoneSelectCmd());
572                                 connector.sendCommand(cmd);
573                             } else {
574                                 success = false;
575                                 logger.debug("Command {} from channel {} failed: undefined source command", command,
576                                         channel);
577                             }
578                         } else {
579                             success = false;
580                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
581                         }
582                         break;
583                     case CHANNEL_ZONE3_SOURCE:
584                         if (!powerZone3) {
585                             success = false;
586                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
587                         } else if (connector.getModel().hasZone3Commands()) {
588                             src = connector.getModel().getSourceFromName(command.toString());
589                             cmd = src.getZone3Command();
590                             if (cmd != null) {
591                                 connector.sendCommand(cmd);
592                             } else {
593                                 success = false;
594                                 logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
595                                         command, channel);
596                             }
597                         } else {
598                             success = false;
599                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
600                         }
601                         break;
602                     case CHANNEL_ZONE4_SOURCE:
603                         if (!powerZone4) {
604                             success = false;
605                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
606                         } else if (connector.getModel().hasZone4Commands()) {
607                             src = connector.getModel().getSourceFromName(command.toString());
608                             cmd = src.getZone4Command();
609                             if (cmd != null) {
610                                 connector.sendCommand(cmd);
611                             } else {
612                                 success = false;
613                                 logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
614                                         command, channel);
615                             }
616                         } else {
617                             success = false;
618                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
619                         }
620                         break;
621                     case CHANNEL_DSP:
622                     case CHANNEL_MAIN_DSP:
623                         if (!isPowerOn()) {
624                             success = false;
625                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
626                         } else {
627                             connector.sendCommand(connector.getModel().getCommandFromDspName(command.toString()));
628                         }
629                         break;
630                     case CHANNEL_VOLUME:
631                     case CHANNEL_MAIN_VOLUME:
632                         if (!isPowerOn()) {
633                             success = false;
634                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
635                         } else if (connector.getModel().hasVolumeControl()) {
636                             handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
637                                     RotelCommand.VOLUME_SET);
638                         } else {
639                             success = false;
640                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
641                         }
642                         break;
643                     case CHANNEL_MAIN_VOLUME_UP_DOWN:
644                         if (!isPowerOn()) {
645                             success = false;
646                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
647                         } else if (connector.getModel().hasVolumeControl()) {
648                             handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
649                                     null);
650                         } else {
651                             success = false;
652                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
653                         }
654                         break;
655                     case CHANNEL_ZONE2_VOLUME:
656                         if (!powerZone2) {
657                             success = false;
658                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
659                         } else if (fixedVolumeZone2) {
660                             success = false;
661                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
662                                     channel);
663                         } else if (connector.getModel().hasVolumeControl()
664                                 && connector.getModel().getNbAdditionalZones() >= 1) {
665                             if (connector.getModel().hasZone2Commands()) {
666                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
667                                         RotelCommand.ZONE2_VOLUME_DOWN, RotelCommand.ZONE2_VOLUME_SET);
668                             } else {
669                                 selectZone(2, connector.getModel().getZoneSelectCmd());
670                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
671                                         RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
672                             }
673                         } else {
674                             success = false;
675                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
676                         }
677                         break;
678                     case CHANNEL_ZONE2_VOLUME_UP_DOWN:
679                         if (!powerZone2) {
680                             success = false;
681                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
682                         } else if (fixedVolumeZone2) {
683                             success = false;
684                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
685                                     channel);
686                         } else if (connector.getModel().hasVolumeControl()
687                                 && connector.getModel().getNbAdditionalZones() >= 1) {
688                             if (connector.getModel().hasZone2Commands()) {
689                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
690                                         RotelCommand.ZONE2_VOLUME_DOWN, null);
691                             } else {
692                                 selectZone(2, connector.getModel().getZoneSelectCmd());
693                                 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
694                                         RotelCommand.VOLUME_DOWN, null);
695                             }
696                         } else {
697                             success = false;
698                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
699                         }
700                         break;
701                     case CHANNEL_ZONE3_VOLUME:
702                         if (!powerZone3) {
703                             success = false;
704                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
705                         } else if (fixedVolumeZone3) {
706                             success = false;
707                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
708                                     channel);
709                         } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone3Commands()) {
710                             handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
711                                     RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
712                         } else {
713                             success = false;
714                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
715                         }
716                         break;
717                     case CHANNEL_ZONE4_VOLUME:
718                         if (!powerZone4) {
719                             success = false;
720                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
721                         } else if (fixedVolumeZone4) {
722                             success = false;
723                             logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
724                                     channel);
725                         } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone4Commands()) {
726                             handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
727                                     RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
728                         } else {
729                             success = false;
730                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
731                         }
732                         break;
733                     case CHANNEL_MUTE:
734                     case CHANNEL_MAIN_MUTE:
735                         if (!isPowerOn()) {
736                             success = false;
737                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
738                         } else if (connector.getModel().hasVolumeControl()) {
739                             handleMuteCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
740                                     getMuteOnCommand(), getMuteOffCommand(), getMuteToggleCommand());
741                         } else {
742                             success = false;
743                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
744                         }
745                         break;
746                     case CHANNEL_ZONE2_MUTE:
747                         if (!powerZone2) {
748                             success = false;
749                             logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
750                         } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone2Commands()) {
751                             handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
752                                     RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
753                         } else {
754                             success = false;
755                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
756                         }
757                         break;
758                     case CHANNEL_ZONE3_MUTE:
759                         if (!powerZone3) {
760                             success = false;
761                             logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
762                         } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone3Commands()) {
763                             handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
764                                     RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
765                         } else {
766                             success = false;
767                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
768                         }
769                         break;
770                     case CHANNEL_ZONE4_MUTE:
771                         if (!powerZone4) {
772                             success = false;
773                             logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
774                         } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone4Commands()) {
775                             handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
776                                     RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
777                         } else {
778                             success = false;
779                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
780                         }
781                         break;
782                     case CHANNEL_BASS:
783                     case CHANNEL_MAIN_BASS:
784                         if (!isPowerOn()) {
785                             success = false;
786                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
787                         } else {
788                             handleToneCmd(bass, channel, command, 2, RotelCommand.BASS_UP, RotelCommand.BASS_DOWN,
789                                     RotelCommand.BASS_SET);
790                         }
791                         break;
792                     case CHANNEL_TREBLE:
793                     case CHANNEL_MAIN_TREBLE:
794                         if (!isPowerOn()) {
795                             success = false;
796                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
797                         } else {
798                             handleToneCmd(treble, channel, command, 1, RotelCommand.TREBLE_UP, RotelCommand.TREBLE_DOWN,
799                                     RotelCommand.TREBLE_SET);
800                         }
801                         break;
802                     case CHANNEL_PLAY_CONTROL:
803                         if (!isPowerOn()) {
804                             success = false;
805                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
806                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
807                             connector.sendCommand(RotelCommand.PLAY);
808                         } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
809                             connector.sendCommand(RotelCommand.PAUSE);
810                             if (connector.getProtocol() == RotelProtocol.ASCII_V1
811                                     && connector.getModel() != RotelModel.RCD1570
812                                     && connector.getModel() != RotelModel.RCD1572
813                                     && connector.getModel() != RotelModel.RCX1500) {
814                                 Thread.sleep(50);
815                                 connector.sendCommand(RotelCommand.PLAY_STATUS);
816                             }
817                         } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
818                             connector.sendCommand(RotelCommand.TRACK_FORWARD);
819                         } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
820                             connector.sendCommand(RotelCommand.TRACK_BACKWORD);
821                         } else {
822                             success = false;
823                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
824                         }
825                         break;
826                     case CHANNEL_BRIGHTNESS:
827                         if (!isPowerOn()) {
828                             success = false;
829                             logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
830                         } else if (!connector.getModel().hasDimmerControl()) {
831                             success = false;
832                             logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
833                         } else if (command instanceof PercentType) {
834                             int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
835                                     * (connector.getModel().getDimmerLevelMax()
836                                             - connector.getModel().getDimmerLevelMin()))
837                                     + connector.getModel().getDimmerLevelMin();
838                             connector.sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
839                         } else {
840                             success = false;
841                             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
842                         }
843                         break;
844                     default:
845                         success = false;
846                         logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
847                         break;
848                 }
849                 if (success) {
850                     logger.debug("Command {} from channel {} succeeded", command, channel);
851                 } else {
852                     updateChannelState(channel);
853                 }
854             } catch (RotelException e) {
855                 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
856                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
857                 closeConnection();
858                 scheduleReconnectJob();
859             } catch (InterruptedException e) {
860                 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
861             }
862         }
863     }
864
865     /**
866      * Handle a power ON/OFF command
867      *
868      * @param channel the channel
869      * @param command the received channel command (OnOffType)
870      * @param onCmd the command to be sent to the device to power it ON
871      * @param offCmd the command to be sent to the device to power it OFF
872      *
873      * @throws RotelException in case of communication error with the device
874      */
875     private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
876             throws RotelException {
877         if (command instanceof OnOffType && command == OnOffType.ON) {
878             connector.sendCommand(onCmd);
879         } else if (command instanceof OnOffType && command == OnOffType.OFF) {
880             connector.sendCommand(offCmd);
881         } else {
882             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
883         }
884     }
885
886     /**
887      * Handle a volume command
888      *
889      * @param current the current volume
890      * @param channel the channel
891      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
892      * @param upCmd the command to be sent to the device to increase the volume
893      * @param downCmd the command to be sent to the device to decrease the volume
894      * @param setCmd the command to be sent to the device to set the volume at a value
895      *
896      * @throws RotelException in case of communication error with the device
897      */
898     private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
899             @Nullable RotelCommand setCmd) throws RotelException {
900         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
901             connector.sendCommand(upCmd);
902         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
903             connector.sendCommand(downCmd);
904         } else if (command instanceof DecimalType && setCmd == null) {
905             int value = ((DecimalType) command).intValue();
906             if (value >= minVolume && value <= maxVolume) {
907                 if (value > current) {
908                     connector.sendCommand(upCmd);
909                 } else if (value < current) {
910                     connector.sendCommand(downCmd);
911                 }
912             }
913         } else if (command instanceof PercentType && setCmd != null) {
914             int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
915                     + minVolume;
916             connector.sendCommand(setCmd, value);
917         } else {
918             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
919         }
920     }
921
922     /**
923      * Handle a mute command
924      *
925      * @param onlyToggle true if only the toggle command must be used
926      * @param channel the channel
927      * @param command the received channel command (OnOffType)
928      * @param onCmd the command to be sent to the device to mute
929      * @param offCmd the command to be sent to the device to unmute
930      * @param toggleCmd the command to be sent to the device to toggle the mute state
931      *
932      * @throws RotelException in case of communication error with the device
933      */
934     private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
935             RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
936         if (command instanceof OnOffType) {
937             if (onlyToggle) {
938                 connector.sendCommand(toggleCmd);
939             } else if (command == OnOffType.ON) {
940                 connector.sendCommand(onCmd);
941             } else if (command == OnOffType.OFF) {
942                 connector.sendCommand(offCmd);
943             }
944         } else {
945             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
946         }
947     }
948
949     /**
950      * Handle a tone level adjustment command (bass or treble)
951      *
952      * @param current the current tone level
953      * @param channel the channel
954      * @param command the received channel command (IncreaseDecreaseType or DecimalType)
955      * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
956      * @param upCmd the command to be sent to the device to increase the tone level
957      * @param downCmd the command to be sent to the device to decrease the tone level
958      * @param setCmd the command to be sent to the device to set the tone level at a value
959      *
960      * @throws RotelException in case of communication error with the device
961      * @throws InterruptedException in case of interruption during a thread sleep
962      */
963     private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
964             RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
965         if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
966             selectToneControl(nbSelect);
967             connector.sendCommand(upCmd);
968         } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
969             selectToneControl(nbSelect);
970             connector.sendCommand(downCmd);
971         } else if (command instanceof DecimalType) {
972             int value = ((DecimalType) command).intValue();
973             if (value >= minToneLevel && value <= maxToneLevel) {
974                 if (connector.getProtocol() != RotelProtocol.HEX) {
975                     connector.sendCommand(setCmd, value);
976                 } else if (value > current) {
977                     selectToneControl(nbSelect);
978                     connector.sendCommand(upCmd);
979                 } else if (value < current) {
980                     selectToneControl(nbSelect);
981                     connector.sendCommand(downCmd);
982                 }
983             }
984         } else {
985             logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
986         }
987     }
988
989     /**
990      * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
991      *
992      * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
993      *
994      * @throws RotelException in case of communication error with the device
995      * @throws InterruptedException in case of interruption during a thread sleep
996      */
997     private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
998         // No tone control select command for RSX-1065
999         if (connector.getProtocol() == RotelProtocol.HEX && connector.getModel() != RotelModel.RSX1065) {
1000             selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1001         }
1002     }
1003
1004     /**
1005      * Run a sequence of commands to display a particular zone on the device front panel
1006      *
1007      * @param zone the zone to be displayed (1 for main zone)
1008      * @param selectCommand the command to be sent to the device to switch the display between zones
1009      *
1010      * @throws RotelException in case of communication error with the device
1011      * @throws InterruptedException in case of interruption during a thread sleep
1012      */
1013     private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1014             throws RotelException, InterruptedException {
1015         if (connector.getProtocol() == RotelProtocol.HEX && connector.getModel().getNbAdditionalZones() >= 1
1016                 && zone >= 1 && zone != currentZone && selectCommand != null) {
1017             int nbSelect;
1018             if (zone < currentZone) {
1019                 nbSelect = zone + connector.getModel().getNbAdditionalZones() - currentZone;
1020                 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1021                     nbSelect++;
1022                 }
1023             } else {
1024                 nbSelect = zone - currentZone;
1025                 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1026                         && !selectingRecord) {
1027                     nbSelect++;
1028                 }
1029             }
1030             selectFeature(nbSelect, null, selectCommand);
1031         }
1032     }
1033
1034     /**
1035      * Run a sequence of commands to display a particular feature on the device front panel
1036      *
1037      * @param nbSelect the number of select commands to be run
1038      * @param preCmd the initial command to be sent to the device (before the select commands)
1039      * @param selectCmd the select command to be sent to the device
1040      *
1041      * @throws RotelException in case of communication error with the device
1042      * @throws InterruptedException in case of interruption during a thread sleep
1043      */
1044     private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1045             throws RotelException, InterruptedException {
1046         if (connector.getProtocol() == RotelProtocol.HEX) {
1047             if (preCmd != null) {
1048                 connector.sendCommand(preCmd);
1049                 Thread.sleep(100);
1050             }
1051             for (int i = 1; i <= nbSelect; i++) {
1052                 connector.sendCommand(selectCmd);
1053                 Thread.sleep(200);
1054             }
1055         }
1056     }
1057
1058     /**
1059      * Open the connection with the Rotel device
1060      *
1061      * @return true if the connection is opened successfully or flase if not
1062      */
1063     private synchronized boolean openConnection() {
1064         connector.addEventListener(this);
1065         try {
1066             connector.open();
1067         } catch (RotelException e) {
1068             logger.debug("openConnection() failed: {}", e.getMessage());
1069         }
1070         logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1071         return connector.isConnected();
1072     }
1073
1074     /**
1075      * Close the connection with the Rotel device
1076      */
1077     private synchronized void closeConnection() {
1078         connector.close();
1079         connector.removeEventListener(this);
1080         logger.debug("closeConnection(): disconnected");
1081     }
1082
1083     @Override
1084     public void onNewMessageEvent(EventObject event) {
1085         cancelPowerOffJob();
1086
1087         RotelMessageEvent evt = (RotelMessageEvent) event;
1088         logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1089
1090         String key = evt.getKey();
1091         String value = evt.getValue().trim();
1092         if (!RotelConnector.KEY_ERROR.equals(key)) {
1093             updateStatus(ThingStatus.ONLINE);
1094         }
1095         try {
1096             switch (key) {
1097                 case RotelConnector.KEY_ERROR:
1098                     logger.debug("Reading feedback message failed");
1099                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reading thread ended");
1100                     closeConnection();
1101                     break;
1102                 case RotelConnector.KEY_LINE1:
1103                     frontPanelLine1 = value;
1104                     updateChannelState(CHANNEL_LINE1);
1105                     break;
1106                 case RotelConnector.KEY_LINE2:
1107                     frontPanelLine2 = value;
1108                     updateChannelState(CHANNEL_LINE2);
1109                     break;
1110                 case RotelConnector.KEY_ZONE:
1111                     currentZone = Integer.parseInt(value);
1112                     break;
1113                 case RotelConnector.KEY_RECORD_SEL:
1114                     selectingRecord = RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value);
1115                     break;
1116                 case RotelConnector.KEY_POWER:
1117                     if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1118                         handlePowerOn();
1119                     } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1120                         handlePowerOff();
1121                     } else if (RotelConnector.POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1122                         schedulePowerOffJob(false);
1123                     } else {
1124                         throw new RotelException("Invalid value");
1125                     }
1126                     break;
1127                 case RotelConnector.KEY_POWER_ZONE2:
1128                     if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1129                         handlePowerOnZone2();
1130                     } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1131                         handlePowerOffZone2();
1132                     } else {
1133                         throw new RotelException("Invalid value");
1134                     }
1135                     break;
1136                 case RotelConnector.KEY_POWER_ZONE3:
1137                     if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1138                         handlePowerOnZone3();
1139                     } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1140                         handlePowerOffZone3();
1141                     } else {
1142                         throw new RotelException("Invalid value");
1143                     }
1144                     break;
1145                 case RotelConnector.KEY_POWER_ZONE4:
1146                     if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1147                         handlePowerOnZone4();
1148                     } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1149                         handlePowerOffZone4();
1150                     } else {
1151                         throw new RotelException("Invalid value");
1152                     }
1153                     break;
1154                 case RotelConnector.KEY_VOLUME_MIN:
1155                     minVolume = Integer.parseInt(value);
1156                     if (!connector.getModel().hasDirectVolumeControl()) {
1157                         logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1158                                 minVolume);
1159                     }
1160                     break;
1161                 case RotelConnector.KEY_VOLUME_MAX:
1162                     maxVolume = Integer.parseInt(value);
1163                     if (!connector.getModel().hasDirectVolumeControl()) {
1164                         logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1165                                 maxVolume);
1166                     }
1167                     break;
1168                 case RotelConnector.KEY_VOLUME:
1169                     if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1170                         volume = minVolume;
1171                     } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1172                         volume = maxVolume;
1173                     } else {
1174                         volume = Integer.parseInt(value);
1175                     }
1176                     updateChannelState(CHANNEL_VOLUME);
1177                     updateChannelState(CHANNEL_MAIN_VOLUME);
1178                     updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1179                     break;
1180                 case RotelConnector.KEY_MUTE:
1181                     if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1182                         mute = true;
1183                         updateChannelState(CHANNEL_MUTE);
1184                         updateChannelState(CHANNEL_MAIN_MUTE);
1185                     } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1186                         mute = false;
1187                         updateChannelState(CHANNEL_MUTE);
1188                         updateChannelState(CHANNEL_MAIN_MUTE);
1189                     } else {
1190                         throw new RotelException("Invalid value");
1191                     }
1192                     break;
1193                 case RotelConnector.KEY_VOLUME_ZONE2:
1194                     fixedVolumeZone2 = false;
1195                     if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1196                         fixedVolumeZone2 = true;
1197                     } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1198                         volumeZone2 = minVolume;
1199                     } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1200                         volumeZone2 = maxVolume;
1201                     } else {
1202                         volumeZone2 = Integer.parseInt(value);
1203                     }
1204                     updateChannelState(CHANNEL_ZONE2_VOLUME);
1205                     updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1206                     break;
1207                 case RotelConnector.KEY_VOLUME_ZONE3:
1208                     fixedVolumeZone3 = false;
1209                     if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1210                         fixedVolumeZone3 = true;
1211                     } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1212                         volumeZone3 = minVolume;
1213                     } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1214                         volumeZone3 = maxVolume;
1215                     } else {
1216                         volumeZone3 = Integer.parseInt(value);
1217                     }
1218                     updateChannelState(CHANNEL_ZONE3_VOLUME);
1219                     break;
1220                 case RotelConnector.KEY_VOLUME_ZONE4:
1221                     fixedVolumeZone4 = false;
1222                     if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1223                         fixedVolumeZone4 = true;
1224                     } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1225                         volumeZone4 = minVolume;
1226                     } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1227                         volumeZone4 = maxVolume;
1228                     } else {
1229                         volumeZone4 = Integer.parseInt(value);
1230                     }
1231                     updateChannelState(CHANNEL_ZONE4_VOLUME);
1232                     break;
1233                 case RotelConnector.KEY_MUTE_ZONE2:
1234                     if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1235                         muteZone2 = true;
1236                         updateChannelState(CHANNEL_ZONE2_MUTE);
1237                     } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1238                         muteZone2 = false;
1239                         updateChannelState(CHANNEL_ZONE2_MUTE);
1240                     } else {
1241                         throw new RotelException("Invalid value");
1242                     }
1243                     break;
1244                 case RotelConnector.KEY_MUTE_ZONE3:
1245                     if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1246                         muteZone3 = true;
1247                         updateChannelState(CHANNEL_ZONE3_MUTE);
1248                     } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1249                         muteZone3 = false;
1250                         updateChannelState(CHANNEL_ZONE3_MUTE);
1251                     } else {
1252                         throw new RotelException("Invalid value");
1253                     }
1254                     break;
1255                 case RotelConnector.KEY_MUTE_ZONE4:
1256                     if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1257                         muteZone4 = true;
1258                         updateChannelState(CHANNEL_ZONE4_MUTE);
1259                     } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1260                         muteZone4 = false;
1261                         updateChannelState(CHANNEL_ZONE4_MUTE);
1262                     } else {
1263                         throw new RotelException("Invalid value");
1264                     }
1265                     break;
1266                 case RotelConnector.KEY_TONE_MAX:
1267                     maxToneLevel = Integer.parseInt(value);
1268                     minToneLevel = -maxToneLevel;
1269                     logger.info(
1270                             "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1271                             minToneLevel, maxToneLevel);
1272                     break;
1273                 case RotelConnector.KEY_BASS:
1274                     if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1275                         bass = minToneLevel;
1276                     } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1277                         bass = maxToneLevel;
1278                     } else {
1279                         bass = Integer.parseInt(value);
1280                     }
1281                     updateChannelState(CHANNEL_BASS);
1282                     updateChannelState(CHANNEL_MAIN_BASS);
1283                     break;
1284                 case RotelConnector.KEY_TREBLE:
1285                     if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1286                         treble = minToneLevel;
1287                     } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1288                         treble = maxToneLevel;
1289                     } else {
1290                         treble = Integer.parseInt(value);
1291                     }
1292                     updateChannelState(CHANNEL_TREBLE);
1293                     updateChannelState(CHANNEL_MAIN_TREBLE);
1294                     break;
1295                 case RotelConnector.KEY_SOURCE:
1296                     source = connector.getModel().getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1297                     updateChannelState(CHANNEL_SOURCE);
1298                     updateChannelState(CHANNEL_MAIN_SOURCE);
1299                     break;
1300                 case RotelConnector.KEY_RECORD:
1301                     recordSource = connector.getModel()
1302                             .getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1303                     updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1304                     break;
1305                 case RotelConnector.KEY_SOURCE_ZONE2:
1306                     sourceZone2 = connector.getModel()
1307                             .getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1308                     updateChannelState(CHANNEL_ZONE2_SOURCE);
1309                     break;
1310                 case RotelConnector.KEY_SOURCE_ZONE3:
1311                     sourceZone3 = connector.getModel()
1312                             .getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1313                     updateChannelState(CHANNEL_ZONE3_SOURCE);
1314                     break;
1315                 case RotelConnector.KEY_SOURCE_ZONE4:
1316                     sourceZone4 = connector.getModel()
1317                             .getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1318                     updateChannelState(CHANNEL_ZONE4_SOURCE);
1319                     break;
1320                 case RotelConnector.KEY_DSP_MODE:
1321                     if ("dolby_pliix_movie".equals(value)) {
1322                         value = "dolby_plii_movie";
1323                     } else if ("dolby_pliix_music".equals(value)) {
1324                         value = "dolby_plii_music";
1325                     } else if ("dolby_pliix_game".equals(value)) {
1326                         value = "dolby_plii_game";
1327                     }
1328                     dsp = connector.getModel().getDspFromFeedback(value);
1329                     logger.debug("DSP {}", dsp.getName());
1330                     updateChannelState(CHANNEL_DSP);
1331                     updateChannelState(CHANNEL_MAIN_DSP);
1332                     break;
1333                 case RotelConnector.KEY1_PLAY_STATUS:
1334                 case RotelConnector.KEY2_PLAY_STATUS:
1335                     if (RotelConnector.PLAY.equalsIgnoreCase(value)) {
1336                         playStatus = RotelPlayStatus.PLAYING;
1337                         updateChannelState(CHANNEL_PLAY_CONTROL);
1338                     } else if (RotelConnector.PAUSE.equalsIgnoreCase(value)) {
1339                         playStatus = RotelPlayStatus.PAUSED;
1340                         updateChannelState(CHANNEL_PLAY_CONTROL);
1341                     } else if (RotelConnector.STOP.equalsIgnoreCase(value)) {
1342                         playStatus = RotelPlayStatus.STOPPED;
1343                         updateChannelState(CHANNEL_PLAY_CONTROL);
1344                     } else {
1345                         throw new RotelException("Invalid value");
1346                     }
1347                     break;
1348                 case RotelConnector.KEY_TRACK:
1349                     if (source.getName().equals("CD") && !connector.getModel().hasSourceControl()) {
1350                         track = Integer.parseInt(value);
1351                         updateChannelState(CHANNEL_TRACK);
1352                     }
1353                     break;
1354                 case RotelConnector.KEY_FREQ:
1355                     if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1356                         frequency = 0.0;
1357                     } else {
1358                         // Suppress a potential ending "k" or "K"
1359                         if (value.toUpperCase().endsWith("K")) {
1360                             value = value.substring(0, value.length() - 1);
1361                         }
1362                         frequency = Double.parseDouble(value);
1363                     }
1364                     updateChannelState(CHANNEL_FREQUENCY);
1365                     break;
1366                 case RotelConnector.KEY_DIMMER:
1367                     brightness = Integer.parseInt(value);
1368                     updateChannelState(CHANNEL_BRIGHTNESS);
1369                     break;
1370                 case RotelConnector.KEY_UPDATE_MODE:
1371                 case RotelConnector.KEY_DISPLAY_UPDATE:
1372                     break;
1373                 default:
1374                     logger.debug("onNewMessageEvent: unhandled key {}", key);
1375                     break;
1376             }
1377         } catch (NumberFormatException | RotelException e) {
1378             logger.debug("Invalid value {} for key {}", value, key);
1379         }
1380     }
1381
1382     /**
1383      * Handle the received information that device power (main zone) is ON
1384      */
1385     private void handlePowerOn() {
1386         Boolean prev = power;
1387         power = true;
1388         updateChannelState(CHANNEL_POWER);
1389         updateChannelState(CHANNEL_MAIN_POWER);
1390         if ((prev == null) || !prev) {
1391             schedulePowerOnJob();
1392         }
1393     }
1394
1395     /**
1396      * Handle the received information that device power (main zone) is OFF
1397      */
1398     private void handlePowerOff() {
1399         cancelPowerOnJob();
1400         power = false;
1401         updateChannelState(CHANNEL_POWER);
1402         updateChannelState(CHANNEL_MAIN_POWER);
1403         updateChannelState(CHANNEL_SOURCE);
1404         updateChannelState(CHANNEL_MAIN_SOURCE);
1405         updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1406         updateChannelState(CHANNEL_DSP);
1407         updateChannelState(CHANNEL_MAIN_DSP);
1408         updateChannelState(CHANNEL_VOLUME);
1409         updateChannelState(CHANNEL_MAIN_VOLUME);
1410         updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1411         updateChannelState(CHANNEL_MUTE);
1412         updateChannelState(CHANNEL_MAIN_MUTE);
1413         updateChannelState(CHANNEL_BASS);
1414         updateChannelState(CHANNEL_MAIN_BASS);
1415         updateChannelState(CHANNEL_TREBLE);
1416         updateChannelState(CHANNEL_MAIN_TREBLE);
1417         updateChannelState(CHANNEL_PLAY_CONTROL);
1418         updateChannelState(CHANNEL_TRACK);
1419         updateChannelState(CHANNEL_FREQUENCY);
1420         updateChannelState(CHANNEL_BRIGHTNESS);
1421     }
1422
1423     /**
1424      * Handle the received information that zone 2 power is ON
1425      */
1426     private void handlePowerOnZone2() {
1427         boolean prev = powerZone2;
1428         powerZone2 = true;
1429         updateChannelState(CHANNEL_ZONE2_POWER);
1430         if (!prev) {
1431             schedulePowerOnZone2Job();
1432         }
1433     }
1434
1435     /**
1436      * Handle the received information that zone 2 power is OFF
1437      */
1438     private void handlePowerOffZone2() {
1439         cancelPowerOnZone2Job();
1440         powerZone2 = false;
1441         updateChannelState(CHANNEL_ZONE2_POWER);
1442         updateChannelState(CHANNEL_ZONE2_SOURCE);
1443         updateChannelState(CHANNEL_ZONE2_VOLUME);
1444         updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1445         updateChannelState(CHANNEL_ZONE2_MUTE);
1446     }
1447
1448     /**
1449      * Handle the received information that zone 3 power is ON
1450      */
1451     private void handlePowerOnZone3() {
1452         boolean prev = powerZone3;
1453         powerZone3 = true;
1454         updateChannelState(CHANNEL_ZONE3_POWER);
1455         if (!prev) {
1456             schedulePowerOnZone3Job();
1457         }
1458     }
1459
1460     /**
1461      * Handle the received information that zone 3 power is OFF
1462      */
1463     private void handlePowerOffZone3() {
1464         cancelPowerOnZone3Job();
1465         powerZone3 = false;
1466         updateChannelState(CHANNEL_ZONE3_POWER);
1467         updateChannelState(CHANNEL_ZONE3_SOURCE);
1468         updateChannelState(CHANNEL_ZONE3_VOLUME);
1469         updateChannelState(CHANNEL_ZONE3_MUTE);
1470     }
1471
1472     /**
1473      * Handle the received information that zone 4 power is ON
1474      */
1475     private void handlePowerOnZone4() {
1476         boolean prev = powerZone4;
1477         powerZone4 = true;
1478         updateChannelState(CHANNEL_ZONE4_POWER);
1479         if (!prev) {
1480             schedulePowerOnZone4Job();
1481         }
1482     }
1483
1484     /**
1485      * Handle the received information that zone 4 power is OFF
1486      */
1487     private void handlePowerOffZone4() {
1488         cancelPowerOnZone4Job();
1489         powerZone4 = false;
1490         updateChannelState(CHANNEL_ZONE4_POWER);
1491         updateChannelState(CHANNEL_ZONE4_SOURCE);
1492         updateChannelState(CHANNEL_ZONE4_VOLUME);
1493         updateChannelState(CHANNEL_ZONE4_MUTE);
1494     }
1495
1496     /**
1497      * Schedule the job that will consider the device as OFF if no new event is received before its running
1498      *
1499      * @param switchOffAllZones true if all zones have to be considered as OFF
1500      */
1501     private void schedulePowerOffJob(boolean switchOffAllZones) {
1502         logger.debug("Schedule power OFF job");
1503         cancelPowerOffJob();
1504         powerOffJob = scheduler.schedule(() -> {
1505             logger.debug("Power OFF job");
1506             handlePowerOff();
1507             if (switchOffAllZones) {
1508                 handlePowerOffZone2();
1509                 handlePowerOffZone3();
1510                 handlePowerOffZone4();
1511             }
1512         }, 2000, TimeUnit.MILLISECONDS);
1513     }
1514
1515     /**
1516      * Cancel the job that will consider the device as OFF
1517      */
1518     private void cancelPowerOffJob() {
1519         ScheduledFuture<?> powerOffJob = this.powerOffJob;
1520         if (powerOffJob != null && !powerOffJob.isCancelled()) {
1521             powerOffJob.cancel(true);
1522             this.powerOffJob = null;
1523         }
1524     }
1525
1526     /**
1527      * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1528      */
1529     private void schedulePowerOnJob() {
1530         logger.debug("Schedule power ON job");
1531         cancelPowerOnJob();
1532         powerOnJob = scheduler.schedule(() -> {
1533             synchronized (sequenceLock) {
1534                 logger.debug("Power ON job");
1535                 try {
1536                     switch (connector.getProtocol()) {
1537                         case HEX:
1538                             if (connector.getModel().getRespNbChars() <= 13
1539                                     && connector.getModel().hasVolumeControl()) {
1540                                 connector.sendCommand(getVolumeDownCommand());
1541                                 Thread.sleep(100);
1542                                 connector.sendCommand(getVolumeUpCommand());
1543                                 Thread.sleep(100);
1544                             }
1545                             if (connector.getModel().getNbAdditionalZones() >= 1) {
1546                                 if (currentZone != 1 && connector.getModel()
1547                                         .getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1548                                     selectZone(1, connector.getModel().getZoneSelectCmd());
1549                                 } else if (!selectingRecord) {
1550                                     connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1551                                     Thread.sleep(100);
1552                                 }
1553                             } else {
1554                                 connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1555                                 Thread.sleep(100);
1556                             }
1557                             if (connector.getModel().hasToneControl()) {
1558                                 if (connector.getModel() == RotelModel.RSX1065) {
1559                                     // No tone control select command
1560                                     connector.sendCommand(RotelCommand.TREBLE_DOWN);
1561                                     Thread.sleep(100);
1562                                     connector.sendCommand(RotelCommand.TREBLE_UP);
1563                                     Thread.sleep(100);
1564                                     connector.sendCommand(RotelCommand.BASS_DOWN);
1565                                     Thread.sleep(100);
1566                                     connector.sendCommand(RotelCommand.BASS_UP);
1567                                     Thread.sleep(100);
1568                                 } else {
1569                                     selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1570                                 }
1571                             }
1572                             break;
1573                         case ASCII_V1:
1574                             if (connector.getModel() != RotelModel.RAP1580 && connector.getModel() != RotelModel.RDD1580
1575                                     && connector.getModel() != RotelModel.RSP1576
1576                                     && connector.getModel() != RotelModel.RSP1582) {
1577                                 connector.sendCommand(RotelCommand.UPDATE_AUTO);
1578                                 Thread.sleep(50);
1579                             }
1580                             if (connector.getModel().hasSourceControl()) {
1581                                 connector.sendCommand(RotelCommand.SOURCE);
1582                                 Thread.sleep(50);
1583                             }
1584                             if (connector.getModel().hasVolumeControl() || connector.getModel().hasToneControl()) {
1585                                 if (connector.getModel().hasVolumeControl()
1586                                         && connector.getModel() != RotelModel.RAP1580
1587                                         && connector.getModel() != RotelModel.RSP1576
1588                                         && connector.getModel() != RotelModel.RSP1582) {
1589                                     connector.sendCommand(RotelCommand.VOLUME_GET_MIN);
1590                                     Thread.sleep(50);
1591                                     connector.sendCommand(RotelCommand.VOLUME_GET_MAX);
1592                                     Thread.sleep(50);
1593                                 }
1594                                 if (connector.getModel().hasToneControl()) {
1595                                     connector.sendCommand(RotelCommand.TONE_MAX);
1596                                     Thread.sleep(50);
1597                                 }
1598                                 // Wait enough to be sure to get the min/max values requested just before
1599                                 Thread.sleep(250);
1600                                 if (connector.getModel().hasVolumeControl()) {
1601                                     connector.sendCommand(RotelCommand.VOLUME_GET);
1602                                     Thread.sleep(50);
1603                                     if (connector.getModel() != RotelModel.RA11
1604                                             && connector.getModel() != RotelModel.RA12
1605                                             && connector.getModel() != RotelModel.RCX1500) {
1606                                         connector.sendCommand(RotelCommand.MUTE);
1607                                         Thread.sleep(50);
1608                                     }
1609                                 }
1610                                 if (connector.getModel().hasToneControl()) {
1611                                     connector.sendCommand(RotelCommand.BASS);
1612                                     Thread.sleep(50);
1613                                     connector.sendCommand(RotelCommand.TREBLE);
1614                                     Thread.sleep(50);
1615                                 }
1616                             }
1617                             if (connector.getModel().hasPlayControl()) {
1618                                 if (connector.getModel() != RotelModel.RCD1570
1619                                         && connector.getModel() != RotelModel.RCD1572
1620                                         && (connector.getModel() != RotelModel.RCX1500
1621                                                 || !source.getName().equals("CD"))) {
1622                                     connector.sendCommand(RotelCommand.PLAY_STATUS);
1623                                     Thread.sleep(50);
1624                                 } else {
1625                                     connector.sendCommand(RotelCommand.CD_PLAY_STATUS);
1626                                     Thread.sleep(50);
1627                                 }
1628                             }
1629                             if (connector.getModel().hasDspControl()) {
1630                                 connector.sendCommand(RotelCommand.DSP_MODE);
1631                                 Thread.sleep(50);
1632                             }
1633                             if (connector.getModel().canGetFrequency()) {
1634                                 connector.sendCommand(RotelCommand.FREQUENCY);
1635                                 Thread.sleep(50);
1636                             }
1637                             if (connector.getModel().hasDimmerControl() && connector.getModel().canGetDimmerLevel()) {
1638                                 connector.sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1639                                 Thread.sleep(50);
1640                             }
1641                             break;
1642                         case ASCII_V2:
1643                             connector.sendCommand(RotelCommand.UPDATE_AUTO);
1644                             Thread.sleep(50);
1645                             if (connector.getModel().hasSourceControl()) {
1646                                 connector.sendCommand(RotelCommand.SOURCE);
1647                                 Thread.sleep(50);
1648                             }
1649                             if (connector.getModel().hasVolumeControl()) {
1650                                 connector.sendCommand(RotelCommand.VOLUME_GET);
1651                                 Thread.sleep(50);
1652                                 connector.sendCommand(RotelCommand.MUTE);
1653                                 Thread.sleep(50);
1654                             }
1655                             if (connector.getModel().hasToneControl()) {
1656                                 connector.sendCommand(RotelCommand.BASS);
1657                                 Thread.sleep(50);
1658                                 connector.sendCommand(RotelCommand.TREBLE);
1659                                 Thread.sleep(50);
1660                             }
1661                             if (connector.getModel().hasPlayControl()) {
1662                                 connector.sendCommand(RotelCommand.PLAY_STATUS);
1663                                 Thread.sleep(50);
1664                                 if (source.getName().equals("CD") && !connector.getModel().hasSourceControl()) {
1665                                     connector.sendCommand(RotelCommand.TRACK);
1666                                     Thread.sleep(50);
1667                                 }
1668                             }
1669                             if (connector.getModel().hasDspControl()) {
1670                                 connector.sendCommand(RotelCommand.DSP_MODE);
1671                                 Thread.sleep(50);
1672                             }
1673                             if (connector.getModel().canGetFrequency()) {
1674                                 connector.sendCommand(RotelCommand.FREQUENCY);
1675                                 Thread.sleep(50);
1676                             }
1677                             if (connector.getModel().hasDimmerControl() && connector.getModel().canGetDimmerLevel()) {
1678                                 connector.sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1679                                 Thread.sleep(50);
1680                             }
1681                             break;
1682                     }
1683                 } catch (RotelException e) {
1684                     logger.debug("Init sequence failed: {}", e.getMessage());
1685                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Init sequence failed");
1686                     closeConnection();
1687                 } catch (InterruptedException e) {
1688                     logger.debug("Init sequence interrupted: {}", e.getMessage());
1689                 }
1690             }
1691         }, 2500, TimeUnit.MILLISECONDS);
1692     }
1693
1694     /**
1695      * Cancel the job scheduled when the device power (main zone) switched ON
1696      */
1697     private void cancelPowerOnJob() {
1698         ScheduledFuture<?> powerOnJob = this.powerOnJob;
1699         if (powerOnJob != null && !powerOnJob.isCancelled()) {
1700             powerOnJob.cancel(true);
1701             this.powerOnJob = null;
1702         }
1703     }
1704
1705     /**
1706      * Schedule the job to run with a few seconds delay when the zone 2 power switched ON
1707      */
1708     private void schedulePowerOnZone2Job() {
1709         logger.debug("Schedule power ON zone 2 job");
1710         cancelPowerOnZone2Job();
1711         powerOnZone2Job = scheduler.schedule(() -> {
1712             synchronized (sequenceLock) {
1713                 logger.debug("Power ON zone 2 job");
1714                 try {
1715                     if (connector.getProtocol() == RotelProtocol.HEX
1716                             && connector.getModel().getNbAdditionalZones() >= 1) {
1717                         selectZone(2, connector.getModel().getZoneSelectCmd());
1718                         connector.sendCommand(connector.getModel().hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN
1719                                 : RotelCommand.VOLUME_DOWN);
1720                         Thread.sleep(100);
1721                         connector.sendCommand(connector.getModel().hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP
1722                                 : RotelCommand.VOLUME_UP);
1723                         Thread.sleep(100);
1724                     }
1725                 } catch (RotelException e) {
1726                     logger.debug("Init sequence zone 2 failed: {}", e.getMessage());
1727                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1728                             "Init sequence zone 2 failed");
1729                     closeConnection();
1730                 } catch (InterruptedException e) {
1731                     logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage());
1732                 }
1733             }
1734         }, 2500, TimeUnit.MILLISECONDS);
1735     }
1736
1737     /**
1738      * Cancel the job scheduled when the zone 2 power switched ON
1739      */
1740     private void cancelPowerOnZone2Job() {
1741         ScheduledFuture<?> powerOnZone2Job = this.powerOnZone2Job;
1742         if (powerOnZone2Job != null && !powerOnZone2Job.isCancelled()) {
1743             powerOnZone2Job.cancel(true);
1744             this.powerOnZone2Job = null;
1745         }
1746     }
1747
1748     /**
1749      * Schedule the job to run with a few seconds delay when the zone 3 power switched ON
1750      */
1751     private void schedulePowerOnZone3Job() {
1752         logger.debug("Schedule power ON zone 3 job");
1753         cancelPowerOnZone3Job();
1754         powerOnZone3Job = scheduler.schedule(() -> {
1755             synchronized (sequenceLock) {
1756                 logger.debug("Power ON zone 3 job");
1757                 try {
1758                     if (connector.getProtocol() == RotelProtocol.HEX
1759                             && connector.getModel().getNbAdditionalZones() >= 2) {
1760                         selectZone(3, connector.getModel().getZoneSelectCmd());
1761                         connector.sendCommand(connector.getModel().hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN
1762                                 : RotelCommand.VOLUME_DOWN);
1763                         Thread.sleep(100);
1764                         connector.sendCommand(connector.getModel().hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP
1765                                 : RotelCommand.VOLUME_UP);
1766                         Thread.sleep(100);
1767                     }
1768                 } catch (RotelException e) {
1769                     logger.debug("Init sequence zone 3 failed: {}", e.getMessage());
1770                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1771                             "Init sequence zone 3 failed");
1772                     closeConnection();
1773                 } catch (InterruptedException e) {
1774                     logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage());
1775                 }
1776             }
1777         }, 2500, TimeUnit.MILLISECONDS);
1778     }
1779
1780     /**
1781      * Cancel the job scheduled when the zone 3 power switched ON
1782      */
1783     private void cancelPowerOnZone3Job() {
1784         ScheduledFuture<?> powerOnZone3Job = this.powerOnZone3Job;
1785         if (powerOnZone3Job != null && !powerOnZone3Job.isCancelled()) {
1786             powerOnZone3Job.cancel(true);
1787             this.powerOnZone3Job = null;
1788         }
1789     }
1790
1791     /**
1792      * Schedule the job to run with a few seconds delay when the zone 4 power switched ON
1793      */
1794     private void schedulePowerOnZone4Job() {
1795         logger.debug("Schedule power ON zone 4 job");
1796         cancelPowerOnZone4Job();
1797         powerOnZone4Job = scheduler.schedule(() -> {
1798             synchronized (sequenceLock) {
1799                 logger.debug("Power ON zone 4 job");
1800                 try {
1801                     if (connector.getProtocol() == RotelProtocol.HEX
1802                             && connector.getModel().getNbAdditionalZones() >= 3) {
1803                         selectZone(4, connector.getModel().getZoneSelectCmd());
1804                         connector.sendCommand(connector.getModel().hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN
1805                                 : RotelCommand.VOLUME_DOWN);
1806                         Thread.sleep(100);
1807                         connector.sendCommand(connector.getModel().hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP
1808                                 : RotelCommand.VOLUME_UP);
1809                         Thread.sleep(100);
1810                     }
1811                 } catch (RotelException e) {
1812                     logger.debug("Init sequence zone 4 failed: {}", e.getMessage());
1813                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1814                             "Init sequence zone 4 failed");
1815                     closeConnection();
1816                 } catch (InterruptedException e) {
1817                     logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage());
1818                 }
1819             }
1820         }, 2500, TimeUnit.MILLISECONDS);
1821     }
1822
1823     /**
1824      * Cancel the job scheduled when the zone 4 power switched ON
1825      */
1826     private void cancelPowerOnZone4Job() {
1827         ScheduledFuture<?> powerOnZone4Job = this.powerOnZone4Job;
1828         if (powerOnZone4Job != null && !powerOnZone4Job.isCancelled()) {
1829             powerOnZone4Job.cancel(true);
1830             this.powerOnZone4Job = null;
1831         }
1832     }
1833
1834     /**
1835      * Schedule the reconnection job
1836      */
1837     private void scheduleReconnectJob() {
1838         logger.debug("Schedule reconnect job");
1839         cancelReconnectJob();
1840         reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
1841             if (!connector.isConnected()) {
1842                 logger.debug("Trying to reconnect...");
1843                 closeConnection();
1844                 power = null;
1845                 String error = null;
1846                 if (openConnection()) {
1847                     synchronized (sequenceLock) {
1848                         schedulePowerOffJob(true);
1849                         try {
1850                             connector.sendCommand(connector.getModel().getPowerStateCmd());
1851                         } catch (RotelException e) {
1852                             error = "First command after connection failed";
1853                             logger.debug("{}: {}", error, e.getMessage());
1854                             cancelPowerOffJob();
1855                             closeConnection();
1856                         }
1857                     }
1858                 } else {
1859                     error = "Reconnection failed";
1860                 }
1861                 if (error != null) {
1862                     handlePowerOff();
1863                     handlePowerOffZone2();
1864                     handlePowerOffZone3();
1865                     handlePowerOffZone4();
1866                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
1867                 } else {
1868                     updateStatus(ThingStatus.ONLINE);
1869                 }
1870             }
1871         }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
1872     }
1873
1874     /**
1875      * Cancel the reconnection job
1876      */
1877     private void cancelReconnectJob() {
1878         ScheduledFuture<?> reconnectJob = this.reconnectJob;
1879         if (reconnectJob != null && !reconnectJob.isCancelled()) {
1880             reconnectJob.cancel(true);
1881             this.reconnectJob = null;
1882         }
1883     }
1884
1885     /**
1886      * Update the state of a channel
1887      *
1888      * @param channel the channel
1889      */
1890     private void updateChannelState(String channel) {
1891         if (!isLinked(channel)) {
1892             return;
1893         }
1894         State state = UnDefType.UNDEF;
1895         switch (channel) {
1896             case CHANNEL_POWER:
1897             case CHANNEL_MAIN_POWER:
1898                 if (power != null) {
1899                     state = power ? OnOffType.ON : OnOffType.OFF;
1900                 }
1901                 break;
1902             case CHANNEL_ZONE2_POWER:
1903                 state = powerZone2 ? OnOffType.ON : OnOffType.OFF;
1904                 break;
1905             case CHANNEL_ZONE3_POWER:
1906                 state = powerZone3 ? OnOffType.ON : OnOffType.OFF;
1907                 break;
1908             case CHANNEL_ZONE4_POWER:
1909                 state = powerZone4 ? OnOffType.ON : OnOffType.OFF;
1910                 break;
1911             case CHANNEL_SOURCE:
1912             case CHANNEL_MAIN_SOURCE:
1913                 if (isPowerOn()) {
1914                     state = new StringType(source.getName());
1915                 }
1916                 break;
1917             case CHANNEL_MAIN_RECORD_SOURCE:
1918                 RotelSource recordSource = this.recordSource;
1919                 if (isPowerOn() && recordSource != null) {
1920                     state = new StringType(recordSource.getName());
1921                 }
1922                 break;
1923             case CHANNEL_ZONE2_SOURCE:
1924                 RotelSource sourceZone2 = this.sourceZone2;
1925                 if (powerZone2 && sourceZone2 != null) {
1926                     state = new StringType(sourceZone2.getName());
1927                 }
1928                 break;
1929             case CHANNEL_ZONE3_SOURCE:
1930                 RotelSource sourceZone3 = this.sourceZone3;
1931                 if (powerZone3 && sourceZone3 != null) {
1932                     state = new StringType(sourceZone3.getName());
1933                 }
1934                 break;
1935             case CHANNEL_ZONE4_SOURCE:
1936                 RotelSource sourceZone4 = this.sourceZone4;
1937                 if (powerZone4 && sourceZone4 != null) {
1938                     state = new StringType(sourceZone4.getName());
1939                 }
1940                 break;
1941             case CHANNEL_DSP:
1942             case CHANNEL_MAIN_DSP:
1943                 if (isPowerOn()) {
1944                     state = new StringType(dsp.getName());
1945                 }
1946                 break;
1947             case CHANNEL_VOLUME:
1948             case CHANNEL_MAIN_VOLUME:
1949                 if (isPowerOn()) {
1950                     long volumePct = Math
1951                             .round((double) (volume - minVolume) / (double) (maxVolume - minVolume) * 100.0);
1952                     state = new PercentType(BigDecimal.valueOf(volumePct));
1953                 }
1954                 break;
1955             case CHANNEL_MAIN_VOLUME_UP_DOWN:
1956                 if (isPowerOn()) {
1957                     state = new DecimalType(volume);
1958                 }
1959                 break;
1960             case CHANNEL_ZONE2_VOLUME:
1961                 if (powerZone2 && !fixedVolumeZone2) {
1962                     long volumePct = Math
1963                             .round((double) (volumeZone2 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
1964                     state = new PercentType(BigDecimal.valueOf(volumePct));
1965                 }
1966                 break;
1967             case CHANNEL_ZONE2_VOLUME_UP_DOWN:
1968                 if (powerZone2 && !fixedVolumeZone2) {
1969                     state = new DecimalType(volumeZone2);
1970                 }
1971                 break;
1972             case CHANNEL_ZONE3_VOLUME:
1973                 if (powerZone3 && !fixedVolumeZone3) {
1974                     long volumePct = Math
1975                             .round((double) (volumeZone3 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
1976                     state = new PercentType(BigDecimal.valueOf(volumePct));
1977                 }
1978                 break;
1979             case CHANNEL_ZONE4_VOLUME:
1980                 if (powerZone4 && !fixedVolumeZone4) {
1981                     long volumePct = Math
1982                             .round((double) (volumeZone4 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
1983                     state = new PercentType(BigDecimal.valueOf(volumePct));
1984                 }
1985                 break;
1986             case CHANNEL_MUTE:
1987             case CHANNEL_MAIN_MUTE:
1988                 if (isPowerOn()) {
1989                     state = mute ? OnOffType.ON : OnOffType.OFF;
1990                 }
1991                 break;
1992             case CHANNEL_ZONE2_MUTE:
1993                 if (powerZone2) {
1994                     state = muteZone2 ? OnOffType.ON : OnOffType.OFF;
1995                 }
1996                 break;
1997             case CHANNEL_ZONE3_MUTE:
1998                 if (powerZone3) {
1999                     state = muteZone3 ? OnOffType.ON : OnOffType.OFF;
2000                 }
2001                 break;
2002             case CHANNEL_ZONE4_MUTE:
2003                 if (powerZone4) {
2004                     state = muteZone4 ? OnOffType.ON : OnOffType.OFF;
2005                 }
2006                 break;
2007             case CHANNEL_BASS:
2008             case CHANNEL_MAIN_BASS:
2009                 if (isPowerOn()) {
2010                     state = new DecimalType(bass);
2011                 }
2012                 break;
2013             case CHANNEL_TREBLE:
2014             case CHANNEL_MAIN_TREBLE:
2015                 if (isPowerOn()) {
2016                     state = new DecimalType(treble);
2017                 }
2018                 break;
2019             case CHANNEL_TRACK:
2020                 if (track > 0 && isPowerOn()) {
2021                     state = new DecimalType(track);
2022                 }
2023                 break;
2024             case CHANNEL_PLAY_CONTROL:
2025                 if (isPowerOn()) {
2026                     switch (playStatus) {
2027                         case PLAYING:
2028                             state = PlayPauseType.PLAY;
2029                             break;
2030                         case PAUSED:
2031                         case STOPPED:
2032                             state = PlayPauseType.PAUSE;
2033                             break;
2034                     }
2035                 }
2036                 break;
2037             case CHANNEL_FREQUENCY:
2038                 if (frequency > 0.0 && isPowerOn()) {
2039                     state = new DecimalType(frequency);
2040                 }
2041                 break;
2042             case CHANNEL_LINE1:
2043                 state = new StringType(frontPanelLine1);
2044                 break;
2045             case CHANNEL_LINE2:
2046                 state = new StringType(frontPanelLine2);
2047                 break;
2048             case CHANNEL_BRIGHTNESS:
2049                 if (isPowerOn() && connector.getModel().hasDimmerControl()) {
2050                     long dimmerPct = Math.round((double) (brightness - connector.getModel().getDimmerLevelMin())
2051                             / (double) (connector.getModel().getDimmerLevelMax()
2052                                     - connector.getModel().getDimmerLevelMin())
2053                             * 100.0);
2054                     state = new PercentType(BigDecimal.valueOf(dimmerPct));
2055                 }
2056                 break;
2057             default:
2058                 break;
2059         }
2060         updateState(channel, state);
2061     }
2062
2063     /**
2064      * Inform about the main zone power state
2065      *
2066      * @return true if main zone power state is known and known as ON
2067      */
2068     private boolean isPowerOn() {
2069         Boolean power = this.power;
2070         return power != null && power.booleanValue();
2071     }
2072
2073     /**
2074      * Get the command to be used for main zone POWER ON
2075      *
2076      * @return the command
2077      */
2078     private RotelCommand getPowerOnCommand() {
2079         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON
2080                 : RotelCommand.POWER_ON;
2081     }
2082
2083     /**
2084      * Get the command to be used for main zone POWER OFF
2085      *
2086      * @return the command
2087      */
2088     private RotelCommand getPowerOffCommand() {
2089         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF
2090                 : RotelCommand.POWER_OFF;
2091     }
2092
2093     /**
2094      * Get the command to be used for main zone VOLUME UP
2095      *
2096      * @return the command
2097      */
2098     private RotelCommand getVolumeUpCommand() {
2099         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP
2100                 : RotelCommand.VOLUME_UP;
2101     }
2102
2103     /**
2104      * Get the command to be used for main zone VOLUME DOWN
2105      *
2106      * @return the command
2107      */
2108     private RotelCommand getVolumeDownCommand() {
2109         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2110                 : RotelCommand.VOLUME_DOWN;
2111     }
2112
2113     /**
2114      * Get the command to be used for main zone MUTE ON
2115      *
2116      * @return the command
2117      */
2118     private RotelCommand getMuteOnCommand() {
2119         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON
2120                 : RotelCommand.MUTE_ON;
2121     }
2122
2123     /**
2124      * Get the command to be used for main zone MUTE OFF
2125      *
2126      * @return the command
2127      */
2128     private RotelCommand getMuteOffCommand() {
2129         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF
2130                 : RotelCommand.MUTE_OFF;
2131     }
2132
2133     /**
2134      * Get the command to be used for main zone MUTE TOGGLE
2135      *
2136      * @return the command
2137      */
2138     private RotelCommand getMuteToggleCommand() {
2139         return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2140                 : RotelCommand.MUTE_TOGGLE;
2141     }
2142 }