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