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