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