2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.rotel.internal.handler;
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
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;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
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.RotelSerialConnector;
38 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSource;
40 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
41 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
42 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
43 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
44 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
45 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
46 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
47 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
48 import org.openhab.core.io.transport.serial.SerialPortManager;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.IncreaseDecreaseType;
51 import org.openhab.core.library.types.NextPreviousType;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.PercentType;
54 import org.openhab.core.library.types.PlayPauseType;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.StateOption;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
72 * @author Laurent Garnier - Initial contribution
75 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
77 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
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 private static final int SLEEP_INTV = 30;
84 private @Nullable ScheduledFuture<?> reconnectJob;
85 private @Nullable ScheduledFuture<?> powerOnJob;
86 private @Nullable ScheduledFuture<?> powerOffJob;
87 private @Nullable ScheduledFuture<?> powerOnZone2Job;
88 private @Nullable ScheduledFuture<?> powerOnZone3Job;
89 private @Nullable ScheduledFuture<?> powerOnZone4Job;
91 private RotelStateDescriptionOptionProvider stateDescriptionProvider;
92 private SerialPortManager serialPortManager;
94 private RotelModel model;
95 private RotelProtocol protocol;
96 private RotelAbstractProtocolHandler protocolHandler;
97 private RotelConnector connector;
99 private int minVolume;
100 private int maxVolume;
101 private int minToneLevel;
102 private int maxToneLevel;
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;
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;
129 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
131 private double frequency;
132 private String frontPanelLine1 = "";
133 private String frontPanelLine2 = "";
134 private int brightness;
135 private boolean tcbypass;
137 private int minBalanceLevel;
138 private int maxBalanceLevel;
139 private boolean speakera;
140 private boolean speakerb;
142 private Object sequenceLock = new Object();
147 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
148 SerialPortManager serialPortManager) {
150 this.stateDescriptionProvider = stateDescriptionProvider;
151 this.serialPortManager = serialPortManager;
152 this.model = DEFAULT_MODEL;
153 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
154 this.protocol = protocolHandler.getProtocol();
155 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
159 public void initialize() {
160 logger.debug("Start initializing handler for thing {}", getThing().getUID());
162 switch (getThing().getThingTypeUID().getId()) {
163 case THING_TYPE_ID_RSP1066:
164 model = RotelModel.RSP1066;
166 case THING_TYPE_ID_RSP1068:
167 model = RotelModel.RSP1068;
169 case THING_TYPE_ID_RSP1069:
170 model = RotelModel.RSP1069;
172 case THING_TYPE_ID_RSP1098:
173 model = RotelModel.RSP1098;
175 case THING_TYPE_ID_RSP1570:
176 model = RotelModel.RSP1570;
178 case THING_TYPE_ID_RSP1572:
179 model = RotelModel.RSP1572;
181 case THING_TYPE_ID_RSX1055:
182 model = RotelModel.RSX1055;
184 case THING_TYPE_ID_RSX1056:
185 model = RotelModel.RSX1056;
187 case THING_TYPE_ID_RSX1057:
188 model = RotelModel.RSX1057;
190 case THING_TYPE_ID_RSX1058:
191 model = RotelModel.RSX1058;
193 case THING_TYPE_ID_RSX1065:
194 model = RotelModel.RSX1065;
196 case THING_TYPE_ID_RSX1067:
197 model = RotelModel.RSX1067;
199 case THING_TYPE_ID_RSX1550:
200 model = RotelModel.RSX1550;
202 case THING_TYPE_ID_RSX1560:
203 model = RotelModel.RSX1560;
205 case THING_TYPE_ID_RSX1562:
206 model = RotelModel.RSX1562;
208 case THING_TYPE_ID_A11:
209 model = RotelModel.A11;
211 case THING_TYPE_ID_A12:
212 model = RotelModel.A12;
214 case THING_TYPE_ID_A14:
215 model = RotelModel.A14;
217 case THING_TYPE_ID_CD11:
218 model = RotelModel.CD11;
220 case THING_TYPE_ID_CD14:
221 model = RotelModel.CD14;
223 case THING_TYPE_ID_RA11:
224 model = RotelModel.RA11;
226 case THING_TYPE_ID_RA12:
227 model = RotelModel.RA12;
229 case THING_TYPE_ID_RA1570:
230 model = RotelModel.RA1570;
232 case THING_TYPE_ID_RA1572:
233 model = RotelModel.RA1572;
235 case THING_TYPE_ID_RA1592:
236 model = RotelModel.RA1592;
238 case THING_TYPE_ID_RAP1580:
239 model = RotelModel.RAP1580;
241 case THING_TYPE_ID_RC1570:
242 model = RotelModel.RC1570;
244 case THING_TYPE_ID_RC1572:
245 model = RotelModel.RC1572;
247 case THING_TYPE_ID_RC1590:
248 model = RotelModel.RC1590;
250 case THING_TYPE_ID_RCD1570:
251 model = RotelModel.RCD1570;
253 case THING_TYPE_ID_RCD1572:
254 model = RotelModel.RCD1572;
256 case THING_TYPE_ID_RCX1500:
257 model = RotelModel.RCX1500;
259 case THING_TYPE_ID_RDD1580:
260 model = RotelModel.RDD1580;
262 case THING_TYPE_ID_RDG1520:
263 case THING_TYPE_ID_RT09:
264 model = RotelModel.RDG1520;
266 case THING_TYPE_ID_RSP1576:
267 model = RotelModel.RSP1576;
269 case THING_TYPE_ID_RSP1582:
270 model = RotelModel.RSP1582;
272 case THING_TYPE_ID_RT11:
273 model = RotelModel.RT11;
275 case THING_TYPE_ID_RT1570:
276 model = RotelModel.RT1570;
278 case THING_TYPE_ID_T11:
279 model = RotelModel.T11;
281 case THING_TYPE_ID_T14:
282 model = RotelModel.T14;
284 case THING_TYPE_ID_M8:
285 model = RotelModel.M8;
287 case THING_TYPE_ID_P5:
288 model = RotelModel.P5;
290 case THING_TYPE_ID_S5:
291 model = RotelModel.S5;
293 case THING_TYPE_ID_X3:
294 model = RotelModel.X3;
296 case THING_TYPE_ID_X5:
297 model = RotelModel.X5;
300 model = DEFAULT_MODEL;
304 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
306 protocol = RotelProtocol.HEX;
307 if (config.protocol != null && !config.protocol.isEmpty()) {
309 protocol = RotelProtocol.getFromName(config.protocol);
310 } catch (RotelException e) {
311 // Invalid protocol name in configuration, HEX will be considered by default
314 Map<String, String> properties = editProperties();
315 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
316 if (property != null && !property.isEmpty()) {
318 protocol = RotelProtocol.getFromName(property);
319 } catch (RotelException e) {
320 // Invalid protocol name in thing property, HEX will be considered by default
324 logger.debug("rotelProtocol {}", protocol.getName());
326 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
327 Map<RotelSource, String> sourcesLabels = new HashMap<>();
329 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
331 if (model.hasVolumeControl()) {
332 maxVolume = model.getVolumeMax();
333 if (!model.hasDirectVolumeControl()) {
335 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
336 minVolume, maxVolume);
339 if (model.hasToneControl()) {
340 maxToneLevel = model.getToneLevelMax();
341 minToneLevel = -maxToneLevel;
343 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
344 minToneLevel, maxToneLevel);
346 if (model.hasBalanceControl()) {
347 maxBalanceLevel = model.getBalanceLevelMax();
348 minBalanceLevel = -maxBalanceLevel;
349 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
350 minBalanceLevel, maxBalanceLevel);
353 // Check configuration settings
354 String configError = null;
355 if ((config.serialPort == null || config.serialPort.isEmpty())
356 && (config.host == null || config.host.isEmpty())) {
357 configError = "@text/offline.config-error-unknown-serialport-and-host";
358 } else if (config.host == null || config.host.isEmpty()) {
359 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
360 configError = "@text/offline.config-error-invalid-serial-over-ip";
363 if (config.port == null) {
364 configError = "@text/offline.config-error-unknown-port";
365 } else if (config.port <= 0) {
366 configError = "@text/offline.config-error-invalid-port";
370 if (configError != null) {
371 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
373 for (RotelSource src : model.getSources()) {
374 // Consider custom input labels
376 switch (src.getName()) {
378 label = config.inputLabelCd;
381 label = config.inputLabelTuner;
384 label = config.inputLabelTape;
387 label = config.inputLabelPhono;
390 label = config.inputLabelVideo1;
393 label = config.inputLabelVideo2;
396 label = config.inputLabelVideo3;
399 label = config.inputLabelVideo4;
402 label = config.inputLabelVideo5;
405 label = config.inputLabelVideo6;
408 label = config.inputLabelUsb;
411 label = config.inputLabelMulti;
416 if (label != null && !label.isEmpty()) {
417 sourcesCustomLabels.put(src, label);
419 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
422 if (protocol == RotelProtocol.HEX) {
423 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
424 } else if (protocol == RotelProtocol.ASCII_V1) {
425 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
427 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
430 if (USE_SIMULATED_DEVICE) {
431 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
432 } else if (config.serialPort != null) {
433 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
434 protocolHandler, readerThreadName);
436 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
439 if (model.hasSourceControl()) {
440 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
441 getStateOptions(model.getSources(), sourcesCustomLabels));
442 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
443 getStateOptions(model.getSources(), sourcesCustomLabels));
444 stateDescriptionProvider.setStateOptions(
445 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
446 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
448 if (model.hasZone2SourceControl()) {
449 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
450 getStateOptions(model.getZone2Sources(), sourcesCustomLabels));
452 if (model.hasZone3SourceControl()) {
453 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
454 getStateOptions(model.getZone3Sources(), sourcesCustomLabels));
456 if (model.hasZone4SourceControl()) {
457 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
458 getStateOptions(model.getZone4Sources(), sourcesCustomLabels));
460 if (model.hasDspControl()) {
461 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
462 model.getDspStateOptions());
463 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
464 model.getDspStateOptions());
467 updateStatus(ThingStatus.UNKNOWN);
469 scheduleReconnectJob();
472 logger.debug("Finished initializing!");
476 public void dispose() {
477 logger.debug("Disposing handler for thing {}", getThing().getUID());
480 cancelPowerOnZone2Job();
481 cancelPowerOnZone3Job();
482 cancelPowerOnZone4Job();
483 cancelReconnectJob();
488 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
489 List<StateOption> options = new ArrayList<>();
490 for (RotelSource item : list) {
491 String label = sourcesLabels.get(item);
492 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
498 public void handleCommand(ChannelUID channelUID, Command command) {
499 String channel = channelUID.getId();
501 if (getThing().getStatus() != ThingStatus.ONLINE) {
502 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
506 if (command instanceof RefreshType) {
507 updateChannelState(channel);
511 if (!connector.isConnected()) {
512 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
518 boolean success = true;
519 synchronized (sequenceLock) {
523 case CHANNEL_MAIN_POWER:
524 handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
526 case CHANNEL_ZONE2_POWER:
527 if (model.hasZone2Commands()) {
528 handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
529 } else if (model.getNbAdditionalZones() == 1) {
530 if (isPowerOn() || powerZone2) {
531 selectZone(2, model.getZoneSelectCmd());
533 sendCommand(RotelCommand.ZONE_SELECT);
536 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
539 case CHANNEL_ZONE3_POWER:
540 if (model.hasZone3Commands()) {
541 handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
544 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
547 case CHANNEL_ZONE4_POWER:
548 if (model.hasZone4Commands()) {
549 handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
552 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
556 case CHANNEL_MAIN_SOURCE:
559 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
561 src = model.getSourceFromName(command.toString());
562 cmd = model.hasOtherThanPrimaryCommands() ? src.getMainZoneCommand() : src.getCommand();
565 if (model.canGetFrequency()) {
566 // send <new-source> returns
567 // 1.) the selected <new-source>
568 // 2.) the used frequency
570 // at response-time the frequency has the value of <old-source>
571 // so we must wait a short moment to get the frequency of <new-source>
573 sendCommand(RotelCommand.FREQUENCY);
575 updateChannelState(CHANNEL_FREQUENCY);
579 logger.debug("Command {} from channel {} failed: undefined source command", command,
584 case CHANNEL_MAIN_RECORD_SOURCE:
587 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
588 } else if (model.hasOtherThanPrimaryCommands()) {
589 src = model.getSourceFromName(command.toString());
590 cmd = src.getRecordCommand();
595 logger.debug("Command {} from channel {} failed: undefined record source command",
599 src = model.getSourceFromName(command.toString());
600 cmd = src.getCommand();
602 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
607 logger.debug("Command {} from channel {} failed: undefined source command", command,
612 case CHANNEL_ZONE2_SOURCE:
615 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
616 } else if (model.hasZone2Commands()) {
617 src = model.getSourceFromName(command.toString());
618 cmd = src.getZone2Command();
623 logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
626 } else if (model.getNbAdditionalZones() >= 1) {
627 src = model.getSourceFromName(command.toString());
628 cmd = src.getCommand();
630 selectZone(2, model.getZoneSelectCmd());
634 logger.debug("Command {} from channel {} failed: undefined source command", command,
639 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
642 case CHANNEL_ZONE3_SOURCE:
645 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
646 } else if (model.hasZone3Commands()) {
647 src = model.getSourceFromName(command.toString());
648 cmd = src.getZone3Command();
653 logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
658 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
661 case CHANNEL_ZONE4_SOURCE:
664 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
665 } else if (model.hasZone4Commands()) {
666 src = model.getSourceFromName(command.toString());
667 cmd = src.getZone4Command();
672 logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
677 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
681 case CHANNEL_MAIN_DSP:
684 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
686 sendCommand(model.getCommandFromDspName(command.toString()));
690 case CHANNEL_MAIN_VOLUME:
693 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
694 } else if (model.hasVolumeControl()) {
695 handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
696 RotelCommand.VOLUME_SET);
699 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
702 case CHANNEL_MAIN_VOLUME_UP_DOWN:
705 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
706 } else if (model.hasVolumeControl()) {
707 handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
711 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
714 case CHANNEL_ZONE2_VOLUME:
717 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
718 } else if (fixedVolumeZone2) {
720 logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
722 } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
723 if (model.hasZone2Commands()) {
724 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
725 RotelCommand.ZONE2_VOLUME_DOWN, RotelCommand.ZONE2_VOLUME_SET);
727 selectZone(2, model.getZoneSelectCmd());
728 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
729 RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
733 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
736 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
739 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
740 } else if (fixedVolumeZone2) {
742 logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
744 } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
745 if (model.hasZone2Commands()) {
746 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
747 RotelCommand.ZONE2_VOLUME_DOWN, null);
749 selectZone(2, model.getZoneSelectCmd());
750 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
751 RotelCommand.VOLUME_DOWN, null);
755 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
758 case CHANNEL_ZONE3_VOLUME:
761 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
762 } else if (fixedVolumeZone3) {
764 logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
766 } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
767 handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
768 RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
771 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
774 case CHANNEL_ZONE4_VOLUME:
777 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
778 } else if (fixedVolumeZone4) {
780 logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
782 } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
783 handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
784 RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
787 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
791 case CHANNEL_MAIN_MUTE:
794 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
795 } else if (model.hasVolumeControl()) {
796 handleMuteCmd(protocol == RotelProtocol.HEX, channel, command, getMuteOnCommand(),
797 getMuteOffCommand(), getMuteToggleCommand());
800 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
803 case CHANNEL_ZONE2_MUTE:
806 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
807 } else if (model.hasVolumeControl() && model.hasZone2Commands()) {
808 handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
809 RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
812 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
815 case CHANNEL_ZONE3_MUTE:
818 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
819 } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
820 handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
821 RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
824 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
827 case CHANNEL_ZONE4_MUTE:
830 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
831 } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
832 handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
833 RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
836 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
840 case CHANNEL_MAIN_BASS:
843 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
844 } else if (tcbypass) {
845 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
847 updateChannelState(CHANNEL_BASS);
849 handleToneCmd(bass, channel, command, 2, RotelCommand.BASS_UP, RotelCommand.BASS_DOWN,
850 RotelCommand.BASS_SET);
854 case CHANNEL_MAIN_TREBLE:
857 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
858 } else if (tcbypass) {
859 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
861 updateChannelState(CHANNEL_TREBLE);
863 handleToneCmd(treble, channel, command, 1, RotelCommand.TREBLE_UP, RotelCommand.TREBLE_DOWN,
864 RotelCommand.TREBLE_SET);
867 case CHANNEL_PLAY_CONTROL:
870 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
871 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
872 sendCommand(RotelCommand.PLAY);
873 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
874 sendCommand(RotelCommand.PAUSE);
875 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
876 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
877 Thread.sleep(SLEEP_INTV);
878 sendCommand(RotelCommand.PLAY_STATUS);
880 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
881 sendCommand(RotelCommand.TRACK_FORWARD);
882 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
883 sendCommand(RotelCommand.TRACK_BACKWORD);
886 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
889 case CHANNEL_BRIGHTNESS:
892 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
893 } else if (!model.hasDimmerControl()) {
895 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
896 } else if (command instanceof PercentType) {
897 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
898 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
899 + model.getDimmerLevelMin();
900 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
903 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
906 case CHANNEL_TCBYPASS:
909 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
910 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
912 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
914 handleTcbypassCmd(channel, command,
915 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
916 : RotelCommand.TCBYPASS_ON,
917 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
918 : RotelCommand.TCBYPASS_OFF);
921 case CHANNEL_BALANCE:
924 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
925 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
927 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
929 handleBalanceCmd(channel, command, RotelCommand.BALANCE_LEFT, RotelCommand.BALANCE_RIGHT,
930 RotelCommand.BALANCE_SET);
933 case CHANNEL_SPEAKER_A:
936 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
938 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
939 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
942 case CHANNEL_SPEAKER_B:
945 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
947 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
948 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
953 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
957 logger.debug("Command {} from channel {} succeeded", command, channel);
959 updateChannelState(channel);
961 } catch (RotelException e) {
962 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
963 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
964 "@text/offline.comm-error-sending-command");
966 scheduleReconnectJob();
967 } catch (InterruptedException e) {
968 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
969 Thread.currentThread().interrupt();
975 * Handle a power ON/OFF command
977 * @param channel the channel
978 * @param command the received channel command (OnOffType)
979 * @param onCmd the command to be sent to the device to power it ON
980 * @param offCmd the command to be sent to the device to power it OFF
982 * @throws RotelException in case of communication error with the device
984 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
985 throws RotelException {
986 if (command instanceof OnOffType && command == OnOffType.ON) {
988 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
991 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
996 * Handle a volume command
998 * @param current the current volume
999 * @param channel the channel
1000 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1001 * @param upCmd the command to be sent to the device to increase the volume
1002 * @param downCmd the command to be sent to the device to decrease the volume
1003 * @param setCmd the command to be sent to the device to set the volume at a value
1005 * @throws RotelException in case of communication error with the device
1007 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
1008 @Nullable RotelCommand setCmd) throws RotelException {
1009 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1011 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1012 sendCommand(downCmd);
1013 } else if (command instanceof DecimalType && setCmd == null) {
1014 int value = ((DecimalType) command).intValue();
1015 if (value >= minVolume && value <= maxVolume) {
1016 if (value > current) {
1018 } else if (value < current) {
1019 sendCommand(downCmd);
1022 } else if (command instanceof PercentType && setCmd != null) {
1023 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1025 sendCommand(setCmd, value);
1027 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1032 * Handle a mute command
1034 * @param onlyToggle true if only the toggle command must be used
1035 * @param channel the channel
1036 * @param command the received channel command (OnOffType)
1037 * @param onCmd the command to be sent to the device to mute
1038 * @param offCmd the command to be sent to the device to unmute
1039 * @param toggleCmd the command to be sent to the device to toggle the mute state
1041 * @throws RotelException in case of communication error with the device
1043 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1044 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1045 if (command instanceof OnOffType) {
1047 sendCommand(toggleCmd);
1048 } else if (command == OnOffType.ON) {
1050 } else if (command == OnOffType.OFF) {
1051 sendCommand(offCmd);
1054 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1059 * Handle a tone level adjustment command (bass or treble)
1061 * @param current the current tone level
1062 * @param channel the channel
1063 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1064 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1065 * @param upCmd the command to be sent to the device to increase the tone level
1066 * @param downCmd the command to be sent to the device to decrease the tone level
1067 * @param setCmd the command to be sent to the device to set the tone level at a value
1069 * @throws RotelException in case of communication error with the device
1070 * @throws InterruptedException in case of interruption during a thread sleep
1072 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1073 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1074 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1075 selectToneControl(nbSelect);
1077 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1078 selectToneControl(nbSelect);
1079 sendCommand(downCmd);
1080 } else if (command instanceof DecimalType) {
1081 int value = ((DecimalType) command).intValue();
1082 if (value >= minToneLevel && value <= maxToneLevel) {
1083 if (protocol != RotelProtocol.HEX) {
1084 sendCommand(setCmd, value);
1085 } else if (value > current) {
1086 selectToneControl(nbSelect);
1088 } else if (value < current) {
1089 selectToneControl(nbSelect);
1090 sendCommand(downCmd);
1094 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1099 * Handle a tcbypass command (only for ASCII protocol)
1101 * @param channel the channel
1102 * @param command the received channel command (OnOffType)
1103 * @param onCmd the command to be sent to the device to bypass_on
1104 * @param offCmd the command to be sent to the device to bypass_off
1106 * @throws RotelException in case of communication error with the device
1108 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1109 throws RotelException, InterruptedException {
1110 if (command instanceof OnOffType) {
1111 if (command == OnOffType.ON) {
1115 updateChannelState(CHANNEL_BASS);
1116 updateChannelState(CHANNEL_TREBLE);
1117 } else if (command == OnOffType.OFF) {
1118 sendCommand(offCmd);
1120 sendCommand(RotelCommand.BASS);
1122 sendCommand(RotelCommand.TREBLE);
1125 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1130 * Handle a speaker command
1132 * @param onlyToggle true if only the toggle command must be used
1133 * @param channel the channel
1134 * @param command the received channel command (OnOffType)
1135 * @param onCmd the command to be sent to the device to speaker_x_on
1136 * @param offCmd the command to be sent to the device to speaker_x_off
1137 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1139 * @throws RotelException in case of communication error with the device
1141 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1142 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1143 if (command instanceof OnOffType) {
1145 sendCommand(toggleCmd);
1146 } else if (command == OnOffType.ON) {
1148 } else if (command == OnOffType.OFF) {
1149 sendCommand(offCmd);
1152 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1157 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1159 * @param channel the channel
1160 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1161 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1162 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1163 * @param setCmd the command to be sent to the device to set the balance at a value
1165 * @throws RotelException in case of communication error with the device
1166 * @throws InterruptedException in case of interruption during a thread sleep
1168 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1169 RotelCommand setCmd) throws RotelException, InterruptedException {
1170 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1171 sendCommand(rightCmd);
1172 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1173 sendCommand(leftCmd);
1174 } else if (command instanceof DecimalType) {
1175 int value = ((DecimalType) command).intValue();
1176 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1177 sendCommand(setCmd, value);
1180 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1185 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1187 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1189 * @throws RotelException in case of communication error with the device
1190 * @throws InterruptedException in case of interruption during a thread sleep
1192 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1193 // No tone control select command for RSX-1065
1194 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1195 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1200 * Run a sequence of commands to display a particular zone on the device front panel
1202 * @param zone the zone to be displayed (1 for main zone)
1203 * @param selectCommand the command to be sent to the device to switch the display between zones
1205 * @throws RotelException in case of communication error with the device
1206 * @throws InterruptedException in case of interruption during a thread sleep
1208 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1209 throws RotelException, InterruptedException {
1210 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1 && zone >= 1 && zone != currentZone
1211 && selectCommand != null) {
1213 if (zone < currentZone) {
1214 nbSelect = zone + model.getNbAdditionalZones() - currentZone;
1215 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1219 nbSelect = zone - currentZone;
1220 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1221 && !selectingRecord) {
1225 selectFeature(nbSelect, null, selectCommand);
1230 * Run a sequence of commands to display a particular feature on the device front panel
1232 * @param nbSelect the number of select commands to be run
1233 * @param preCmd the initial command to be sent to the device (before the select commands)
1234 * @param selectCmd the select command to be sent to the device
1236 * @throws RotelException in case of communication error with the device
1237 * @throws InterruptedException in case of interruption during a thread sleep
1239 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1240 throws RotelException, InterruptedException {
1241 if (protocol == RotelProtocol.HEX) {
1242 if (preCmd != null) {
1243 sendCommand(preCmd);
1246 for (int i = 1; i <= nbSelect; i++) {
1247 sendCommand(selectCmd);
1254 * Open the connection with the Rotel device
1256 * @return true if the connection is opened successfully or flase if not
1258 private synchronized boolean openConnection() {
1259 protocolHandler.addEventListener(this);
1262 } catch (RotelException e) {
1263 logger.debug("openConnection() failed", e);
1265 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1266 return connector.isConnected();
1270 * Close the connection with the Rotel device
1272 private synchronized void closeConnection() {
1274 protocolHandler.removeEventListener(this);
1275 logger.debug("closeConnection(): disconnected");
1279 public void onNewMessageEvent(EventObject event) {
1280 cancelPowerOffJob();
1282 RotelMessageEvent evt = (RotelMessageEvent) event;
1283 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1285 String key = evt.getKey();
1286 String value = evt.getValue().trim();
1287 if (!KEY_ERROR.equals(key)) {
1288 updateStatus(ThingStatus.ONLINE);
1293 logger.debug("Reading feedback message failed");
1294 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1295 "@text/offline.comm-error-reading-thread");
1299 frontPanelLine1 = value;
1300 updateChannelState(CHANNEL_LINE1);
1303 frontPanelLine2 = value;
1304 updateChannelState(CHANNEL_LINE2);
1307 currentZone = Integer.parseInt(value);
1309 case KEY_RECORD_SEL:
1310 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1313 if (POWER_ON.equalsIgnoreCase(value)) {
1315 } else if (STANDBY.equalsIgnoreCase(value)) {
1317 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1318 schedulePowerOffJob(false);
1320 throw new RotelException("Invalid value");
1323 case KEY_POWER_ZONE2:
1324 if (POWER_ON.equalsIgnoreCase(value)) {
1325 handlePowerOnZone2();
1326 } else if (STANDBY.equalsIgnoreCase(value)) {
1327 handlePowerOffZone2();
1329 throw new RotelException("Invalid value");
1332 case KEY_POWER_ZONE3:
1333 if (POWER_ON.equalsIgnoreCase(value)) {
1334 handlePowerOnZone3();
1335 } else if (STANDBY.equalsIgnoreCase(value)) {
1336 handlePowerOffZone3();
1338 throw new RotelException("Invalid value");
1341 case KEY_POWER_ZONE4:
1342 if (POWER_ON.equalsIgnoreCase(value)) {
1343 handlePowerOnZone4();
1344 } else if (STANDBY.equalsIgnoreCase(value)) {
1345 handlePowerOffZone4();
1347 throw new RotelException("Invalid value");
1350 case KEY_VOLUME_MIN:
1351 minVolume = Integer.parseInt(value);
1352 if (!model.hasDirectVolumeControl()) {
1353 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1357 case KEY_VOLUME_MAX:
1358 maxVolume = Integer.parseInt(value);
1359 if (!model.hasDirectVolumeControl()) {
1360 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1365 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1367 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1370 volume = Integer.parseInt(value);
1372 updateChannelState(CHANNEL_VOLUME);
1373 updateChannelState(CHANNEL_MAIN_VOLUME);
1374 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1377 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1379 updateChannelState(CHANNEL_MUTE);
1380 updateChannelState(CHANNEL_MAIN_MUTE);
1381 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1383 updateChannelState(CHANNEL_MUTE);
1384 updateChannelState(CHANNEL_MAIN_MUTE);
1386 throw new RotelException("Invalid value");
1389 case KEY_VOLUME_ZONE2:
1390 fixedVolumeZone2 = false;
1391 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1392 fixedVolumeZone2 = true;
1393 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1394 volumeZone2 = minVolume;
1395 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1396 volumeZone2 = maxVolume;
1398 volumeZone2 = Integer.parseInt(value);
1400 updateChannelState(CHANNEL_ZONE2_VOLUME);
1401 updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1403 case KEY_VOLUME_ZONE3:
1404 fixedVolumeZone3 = false;
1405 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1406 fixedVolumeZone3 = true;
1407 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1408 volumeZone3 = minVolume;
1409 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1410 volumeZone3 = maxVolume;
1412 volumeZone3 = Integer.parseInt(value);
1414 updateChannelState(CHANNEL_ZONE3_VOLUME);
1416 case KEY_VOLUME_ZONE4:
1417 fixedVolumeZone4 = false;
1418 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1419 fixedVolumeZone4 = true;
1420 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1421 volumeZone4 = minVolume;
1422 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1423 volumeZone4 = maxVolume;
1425 volumeZone4 = Integer.parseInt(value);
1427 updateChannelState(CHANNEL_ZONE4_VOLUME);
1429 case KEY_MUTE_ZONE2:
1430 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1432 updateChannelState(CHANNEL_ZONE2_MUTE);
1433 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1435 updateChannelState(CHANNEL_ZONE2_MUTE);
1437 throw new RotelException("Invalid value");
1440 case KEY_MUTE_ZONE3:
1441 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1443 updateChannelState(CHANNEL_ZONE3_MUTE);
1444 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1446 updateChannelState(CHANNEL_ZONE3_MUTE);
1448 throw new RotelException("Invalid value");
1451 case KEY_MUTE_ZONE4:
1452 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1454 updateChannelState(CHANNEL_ZONE4_MUTE);
1455 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1457 updateChannelState(CHANNEL_ZONE4_MUTE);
1459 throw new RotelException("Invalid value");
1463 maxToneLevel = Integer.parseInt(value);
1464 minToneLevel = -maxToneLevel;
1466 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1467 minToneLevel, maxToneLevel);
1470 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1471 bass = minToneLevel;
1472 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1473 bass = maxToneLevel;
1475 bass = Integer.parseInt(value);
1477 updateChannelState(CHANNEL_BASS);
1478 updateChannelState(CHANNEL_MAIN_BASS);
1481 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1482 treble = minToneLevel;
1483 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1484 treble = maxToneLevel;
1486 treble = Integer.parseInt(value);
1488 updateChannelState(CHANNEL_TREBLE);
1489 updateChannelState(CHANNEL_MAIN_TREBLE);
1492 source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1493 updateChannelState(CHANNEL_SOURCE);
1494 updateChannelState(CHANNEL_MAIN_SOURCE);
1497 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1498 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1500 case KEY_SOURCE_ZONE2:
1501 sourceZone2 = model.getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1502 updateChannelState(CHANNEL_ZONE2_SOURCE);
1504 case KEY_SOURCE_ZONE3:
1505 sourceZone3 = model.getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1506 updateChannelState(CHANNEL_ZONE3_SOURCE);
1508 case KEY_SOURCE_ZONE4:
1509 sourceZone4 = model.getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1510 updateChannelState(CHANNEL_ZONE4_SOURCE);
1513 if ("dolby_pliix_movie".equals(value)) {
1514 value = "dolby_plii_movie";
1515 } else if ("dolby_pliix_music".equals(value)) {
1516 value = "dolby_plii_music";
1517 } else if ("dolby_pliix_game".equals(value)) {
1518 value = "dolby_plii_game";
1520 dsp = model.getDspFromFeedback(value);
1521 logger.debug("DSP {}", dsp.getName());
1522 updateChannelState(CHANNEL_DSP);
1523 updateChannelState(CHANNEL_MAIN_DSP);
1525 case KEY1_PLAY_STATUS:
1526 case KEY2_PLAY_STATUS:
1527 if (PLAY.equalsIgnoreCase(value)) {
1528 playStatus = RotelPlayStatus.PLAYING;
1529 updateChannelState(CHANNEL_PLAY_CONTROL);
1530 } else if (PAUSE.equalsIgnoreCase(value)) {
1531 playStatus = RotelPlayStatus.PAUSED;
1532 updateChannelState(CHANNEL_PLAY_CONTROL);
1533 } else if (STOP.equalsIgnoreCase(value)) {
1534 playStatus = RotelPlayStatus.STOPPED;
1535 updateChannelState(CHANNEL_PLAY_CONTROL);
1537 throw new RotelException("Invalid value");
1541 if (source.getName().equals("CD") && !model.hasSourceControl()) {
1542 track = Integer.parseInt(value);
1543 updateChannelState(CHANNEL_TRACK);
1547 if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1550 // Suppress a potential ending "k" or "K"
1551 if (value.toUpperCase().endsWith("K")) {
1552 value = value.substring(0, value.length() - 1);
1554 frequency = Double.parseDouble(value);
1556 updateChannelState(CHANNEL_FREQUENCY);
1559 brightness = Integer.parseInt(value);
1560 updateChannelState(CHANNEL_BRIGHTNESS);
1562 case KEY_UPDATE_MODE:
1563 case KEY_DISPLAY_UPDATE:
1566 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1568 updateChannelState(CHANNEL_TCBYPASS);
1569 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1571 updateChannelState(CHANNEL_TCBYPASS);
1573 throw new RotelException("Invalid value");
1577 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1579 updateChannelState(CHANNEL_TCBYPASS);
1580 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1582 updateChannelState(CHANNEL_TCBYPASS);
1584 throw new RotelException("Invalid value");
1588 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1589 balance = minBalanceLevel;
1590 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1591 balance = maxBalanceLevel;
1592 } else if (value.toUpperCase().startsWith("L")) {
1593 balance = -Integer.parseInt(value.substring(1));
1594 } else if (value.toUpperCase().startsWith("R")) {
1595 balance = Integer.parseInt(value.substring(1));
1597 balance = Integer.parseInt(value);
1599 updateChannelState(CHANNEL_BALANCE);
1602 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1605 updateChannelState(CHANNEL_SPEAKER_A);
1606 updateChannelState(CHANNEL_SPEAKER_B);
1607 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1610 updateChannelState(CHANNEL_SPEAKER_A);
1611 updateChannelState(CHANNEL_SPEAKER_B);
1612 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1615 updateChannelState(CHANNEL_SPEAKER_A);
1616 updateChannelState(CHANNEL_SPEAKER_B);
1617 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1620 updateChannelState(CHANNEL_SPEAKER_A);
1621 updateChannelState(CHANNEL_SPEAKER_B);
1623 throw new RotelException("Invalid value");
1627 logger.debug("onNewMessageEvent: unhandled key {}", key);
1630 } catch (NumberFormatException | RotelException e) {
1631 logger.debug("Invalid value {} for key {}", value, key);
1636 * Handle the received information that device power (main zone) is ON
1638 private void handlePowerOn() {
1639 Boolean prev = power;
1641 updateChannelState(CHANNEL_POWER);
1642 updateChannelState(CHANNEL_MAIN_POWER);
1643 if ((prev == null) || !prev) {
1644 schedulePowerOnJob();
1649 * Handle the received information that device power (main zone) is OFF
1651 private void handlePowerOff() {
1654 updateChannelState(CHANNEL_POWER);
1655 updateChannelState(CHANNEL_MAIN_POWER);
1656 updateChannelState(CHANNEL_SOURCE);
1657 updateChannelState(CHANNEL_MAIN_SOURCE);
1658 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1659 updateChannelState(CHANNEL_DSP);
1660 updateChannelState(CHANNEL_MAIN_DSP);
1661 updateChannelState(CHANNEL_VOLUME);
1662 updateChannelState(CHANNEL_MAIN_VOLUME);
1663 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1664 updateChannelState(CHANNEL_MUTE);
1665 updateChannelState(CHANNEL_MAIN_MUTE);
1666 updateChannelState(CHANNEL_BASS);
1667 updateChannelState(CHANNEL_MAIN_BASS);
1668 updateChannelState(CHANNEL_TREBLE);
1669 updateChannelState(CHANNEL_MAIN_TREBLE);
1670 updateChannelState(CHANNEL_PLAY_CONTROL);
1671 updateChannelState(CHANNEL_TRACK);
1672 updateChannelState(CHANNEL_FREQUENCY);
1673 updateChannelState(CHANNEL_BRIGHTNESS);
1674 updateChannelState(CHANNEL_TCBYPASS);
1675 updateChannelState(CHANNEL_BALANCE);
1676 updateChannelState(CHANNEL_SPEAKER_A);
1677 updateChannelState(CHANNEL_SPEAKER_B);
1681 * Handle the received information that zone 2 power is ON
1683 private void handlePowerOnZone2() {
1684 boolean prev = powerZone2;
1686 updateChannelState(CHANNEL_ZONE2_POWER);
1688 schedulePowerOnZone2Job();
1693 * Handle the received information that zone 2 power is OFF
1695 private void handlePowerOffZone2() {
1696 cancelPowerOnZone2Job();
1698 updateChannelState(CHANNEL_ZONE2_POWER);
1699 updateChannelState(CHANNEL_ZONE2_SOURCE);
1700 updateChannelState(CHANNEL_ZONE2_VOLUME);
1701 updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1702 updateChannelState(CHANNEL_ZONE2_MUTE);
1706 * Handle the received information that zone 3 power is ON
1708 private void handlePowerOnZone3() {
1709 boolean prev = powerZone3;
1711 updateChannelState(CHANNEL_ZONE3_POWER);
1713 schedulePowerOnZone3Job();
1718 * Handle the received information that zone 3 power is OFF
1720 private void handlePowerOffZone3() {
1721 cancelPowerOnZone3Job();
1723 updateChannelState(CHANNEL_ZONE3_POWER);
1724 updateChannelState(CHANNEL_ZONE3_SOURCE);
1725 updateChannelState(CHANNEL_ZONE3_VOLUME);
1726 updateChannelState(CHANNEL_ZONE3_MUTE);
1730 * Handle the received information that zone 4 power is ON
1732 private void handlePowerOnZone4() {
1733 boolean prev = powerZone4;
1735 updateChannelState(CHANNEL_ZONE4_POWER);
1737 schedulePowerOnZone4Job();
1742 * Handle the received information that zone 4 power is OFF
1744 private void handlePowerOffZone4() {
1745 cancelPowerOnZone4Job();
1747 updateChannelState(CHANNEL_ZONE4_POWER);
1748 updateChannelState(CHANNEL_ZONE4_SOURCE);
1749 updateChannelState(CHANNEL_ZONE4_VOLUME);
1750 updateChannelState(CHANNEL_ZONE4_MUTE);
1754 * Schedule the job that will consider the device as OFF if no new event is received before its running
1756 * @param switchOffAllZones true if all zones have to be considered as OFF
1758 private void schedulePowerOffJob(boolean switchOffAllZones) {
1759 logger.debug("Schedule power OFF job");
1760 cancelPowerOffJob();
1761 powerOffJob = scheduler.schedule(() -> {
1762 logger.debug("Power OFF job");
1764 if (switchOffAllZones) {
1765 handlePowerOffZone2();
1766 handlePowerOffZone3();
1767 handlePowerOffZone4();
1769 }, 2000, TimeUnit.MILLISECONDS);
1773 * Cancel the job that will consider the device as OFF
1775 private void cancelPowerOffJob() {
1776 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1777 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1778 powerOffJob.cancel(true);
1779 this.powerOffJob = null;
1784 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1786 private void schedulePowerOnJob() {
1787 logger.debug("Schedule power ON job");
1789 powerOnJob = scheduler.schedule(() -> {
1790 synchronized (sequenceLock) {
1791 logger.debug("Power ON job");
1795 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1796 sendCommand(getVolumeDownCommand());
1798 sendCommand(getVolumeUpCommand());
1801 if (model.getNbAdditionalZones() >= 1) {
1802 if (currentZone != 1
1803 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1804 selectZone(1, model.getZoneSelectCmd());
1805 } else if (!selectingRecord) {
1806 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1810 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1813 if (model.hasToneControl()) {
1814 if (model == RotelModel.RSX1065) {
1815 // No tone control select command
1816 sendCommand(RotelCommand.TREBLE_DOWN);
1818 sendCommand(RotelCommand.TREBLE_UP);
1820 sendCommand(RotelCommand.BASS_DOWN);
1822 sendCommand(RotelCommand.BASS_UP);
1825 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1830 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1831 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1832 sendCommand(RotelCommand.UPDATE_AUTO);
1833 Thread.sleep(SLEEP_INTV);
1835 if (model.hasSourceControl()) {
1836 sendCommand(RotelCommand.SOURCE);
1837 Thread.sleep(SLEEP_INTV);
1839 if (model.hasVolumeControl() || model.hasToneControl()) {
1840 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1841 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1842 sendCommand(RotelCommand.VOLUME_GET_MIN);
1843 Thread.sleep(SLEEP_INTV);
1844 sendCommand(RotelCommand.VOLUME_GET_MAX);
1845 Thread.sleep(SLEEP_INTV);
1847 if (model.hasToneControl()) {
1848 sendCommand(RotelCommand.TONE_MAX);
1849 Thread.sleep(SLEEP_INTV);
1851 // Wait enough to be sure to get the min/max values requested just before
1853 if (model.hasVolumeControl()) {
1854 sendCommand(RotelCommand.VOLUME_GET);
1855 Thread.sleep(SLEEP_INTV);
1856 if (model != RotelModel.RA11 && model != RotelModel.RA12
1857 && model != RotelModel.RCX1500) {
1858 sendCommand(RotelCommand.MUTE);
1859 Thread.sleep(SLEEP_INTV);
1862 if (model.hasToneControl()) {
1863 sendCommand(RotelCommand.BASS);
1864 Thread.sleep(SLEEP_INTV);
1865 sendCommand(RotelCommand.TREBLE);
1866 Thread.sleep(SLEEP_INTV);
1867 sendCommand(RotelCommand.TONE_CONTROLS);
1868 Thread.sleep(SLEEP_INTV);
1871 if (model.hasBalanceControl()) {
1872 sendCommand(RotelCommand.BALANCE);
1873 Thread.sleep(SLEEP_INTV);
1875 if (model.hasPlayControl()) {
1876 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1877 && (model != RotelModel.RCX1500 || !source.getName().equals("CD"))) {
1878 sendCommand(RotelCommand.PLAY_STATUS);
1879 Thread.sleep(SLEEP_INTV);
1881 sendCommand(RotelCommand.CD_PLAY_STATUS);
1882 Thread.sleep(SLEEP_INTV);
1885 if (model.hasDspControl()) {
1886 sendCommand(RotelCommand.DSP_MODE);
1887 Thread.sleep(SLEEP_INTV);
1889 if (model.canGetFrequency()) {
1890 sendCommand(RotelCommand.FREQUENCY);
1891 Thread.sleep(SLEEP_INTV);
1893 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1894 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1895 Thread.sleep(SLEEP_INTV);
1897 if (model.hasSpeakerGroups()) {
1898 sendCommand(RotelCommand.SPEAKER);
1899 Thread.sleep(SLEEP_INTV);
1903 sendCommand(RotelCommand.UPDATE_AUTO);
1904 Thread.sleep(SLEEP_INTV);
1905 if (model.hasSourceControl()) {
1906 sendCommand(RotelCommand.SOURCE);
1907 Thread.sleep(SLEEP_INTV);
1909 if (model.hasVolumeControl()) {
1910 sendCommand(RotelCommand.VOLUME_GET);
1911 Thread.sleep(SLEEP_INTV);
1912 sendCommand(RotelCommand.MUTE);
1913 Thread.sleep(SLEEP_INTV);
1915 if (model.hasToneControl()) {
1916 sendCommand(RotelCommand.BASS);
1917 Thread.sleep(SLEEP_INTV);
1918 sendCommand(RotelCommand.TREBLE);
1919 Thread.sleep(SLEEP_INTV);
1920 sendCommand(RotelCommand.TCBYPASS);
1921 Thread.sleep(SLEEP_INTV);
1923 if (model.hasBalanceControl()) {
1924 sendCommand(RotelCommand.BALANCE);
1925 Thread.sleep(SLEEP_INTV);
1927 if (model.hasPlayControl()) {
1928 sendCommand(RotelCommand.PLAY_STATUS);
1929 Thread.sleep(SLEEP_INTV);
1930 if (source.getName().equals("CD") && !model.hasSourceControl()) {
1931 sendCommand(RotelCommand.TRACK);
1932 Thread.sleep(SLEEP_INTV);
1935 if (model.hasDspControl()) {
1936 sendCommand(RotelCommand.DSP_MODE);
1937 Thread.sleep(SLEEP_INTV);
1939 if (model.canGetFrequency()) {
1940 sendCommand(RotelCommand.FREQUENCY);
1941 Thread.sleep(SLEEP_INTV);
1943 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1944 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1945 Thread.sleep(SLEEP_INTV);
1947 if (model.hasSpeakerGroups()) {
1948 sendCommand(RotelCommand.SPEAKER);
1949 Thread.sleep(SLEEP_INTV);
1953 } catch (RotelException e) {
1954 logger.debug("Init sequence failed: {}", e.getMessage());
1955 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1956 "@text/offline.comm-error-init-sequence");
1958 } catch (InterruptedException e) {
1959 logger.debug("Init sequence interrupted: {}", e.getMessage());
1960 Thread.currentThread().interrupt();
1963 }, 2500, TimeUnit.MILLISECONDS);
1967 * Cancel the job scheduled when the device power (main zone) switched ON
1969 private void cancelPowerOnJob() {
1970 ScheduledFuture<?> powerOnJob = this.powerOnJob;
1971 if (powerOnJob != null && !powerOnJob.isCancelled()) {
1972 powerOnJob.cancel(true);
1973 this.powerOnJob = null;
1978 * Schedule the job to run with a few seconds delay when the zone 2 power switched ON
1980 private void schedulePowerOnZone2Job() {
1981 logger.debug("Schedule power ON zone 2 job");
1982 cancelPowerOnZone2Job();
1983 powerOnZone2Job = scheduler.schedule(() -> {
1984 synchronized (sequenceLock) {
1985 logger.debug("Power ON zone 2 job");
1987 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1) {
1988 selectZone(2, model.getZoneSelectCmd());
1990 model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
1992 sendCommand(model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP : RotelCommand.VOLUME_UP);
1995 } catch (RotelException e) {
1996 logger.debug("Init sequence zone 2 failed: {}", e.getMessage());
1997 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1998 "@text/offline.comm-error-init-sequence-zone [\"2\"]");
2000 } catch (InterruptedException e) {
2001 logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage());
2002 Thread.currentThread().interrupt();
2005 }, 2500, TimeUnit.MILLISECONDS);
2009 * Cancel the job scheduled when the zone 2 power switched ON
2011 private void cancelPowerOnZone2Job() {
2012 ScheduledFuture<?> powerOnZone2Job = this.powerOnZone2Job;
2013 if (powerOnZone2Job != null && !powerOnZone2Job.isCancelled()) {
2014 powerOnZone2Job.cancel(true);
2015 this.powerOnZone2Job = null;
2020 * Schedule the job to run with a few seconds delay when the zone 3 power switched ON
2022 private void schedulePowerOnZone3Job() {
2023 logger.debug("Schedule power ON zone 3 job");
2024 cancelPowerOnZone3Job();
2025 powerOnZone3Job = scheduler.schedule(() -> {
2026 synchronized (sequenceLock) {
2027 logger.debug("Power ON zone 3 job");
2029 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 2) {
2030 selectZone(3, model.getZoneSelectCmd());
2032 model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
2034 sendCommand(model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP : RotelCommand.VOLUME_UP);
2037 } catch (RotelException e) {
2038 logger.debug("Init sequence zone 3 failed: {}", e.getMessage());
2039 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2040 "@text/offline.comm-error-init-sequence-zone [\"3\"]");
2042 } catch (InterruptedException e) {
2043 logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage());
2044 Thread.currentThread().interrupt();
2047 }, 2500, TimeUnit.MILLISECONDS);
2051 * Cancel the job scheduled when the zone 3 power switched ON
2053 private void cancelPowerOnZone3Job() {
2054 ScheduledFuture<?> powerOnZone3Job = this.powerOnZone3Job;
2055 if (powerOnZone3Job != null && !powerOnZone3Job.isCancelled()) {
2056 powerOnZone3Job.cancel(true);
2057 this.powerOnZone3Job = null;
2062 * Schedule the job to run with a few seconds delay when the zone 4 power switched ON
2064 private void schedulePowerOnZone4Job() {
2065 logger.debug("Schedule power ON zone 4 job");
2066 cancelPowerOnZone4Job();
2067 powerOnZone4Job = scheduler.schedule(() -> {
2068 synchronized (sequenceLock) {
2069 logger.debug("Power ON zone 4 job");
2071 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 3) {
2072 selectZone(4, model.getZoneSelectCmd());
2074 model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
2076 sendCommand(model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP : RotelCommand.VOLUME_UP);
2079 } catch (RotelException e) {
2080 logger.debug("Init sequence zone 4 failed: {}", e.getMessage());
2081 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2082 "@text/offline.comm-error-init-sequence-zone [\"4\"]");
2084 } catch (InterruptedException e) {
2085 logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage());
2086 Thread.currentThread().interrupt();
2089 }, 2500, TimeUnit.MILLISECONDS);
2093 * Cancel the job scheduled when the zone 4 power switched ON
2095 private void cancelPowerOnZone4Job() {
2096 ScheduledFuture<?> powerOnZone4Job = this.powerOnZone4Job;
2097 if (powerOnZone4Job != null && !powerOnZone4Job.isCancelled()) {
2098 powerOnZone4Job.cancel(true);
2099 this.powerOnZone4Job = null;
2104 * Schedule the reconnection job
2106 private void scheduleReconnectJob() {
2107 logger.debug("Schedule reconnect job");
2108 cancelReconnectJob();
2109 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2110 if (!connector.isConnected()) {
2111 logger.debug("Trying to reconnect...");
2114 String error = null;
2115 if (openConnection()) {
2116 synchronized (sequenceLock) {
2117 schedulePowerOffJob(true);
2119 sendCommand(model.getPowerStateCmd());
2120 } catch (RotelException e) {
2121 error = "@text/offline.comm-error-first-command-after-reconnection";
2122 logger.debug("First command after connection failed", e);
2123 cancelPowerOffJob();
2128 error = "@text/offline.comm-error-reconnection";
2130 if (error != null) {
2132 handlePowerOffZone2();
2133 handlePowerOffZone3();
2134 handlePowerOffZone4();
2135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2137 updateStatus(ThingStatus.ONLINE);
2140 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2144 * Cancel the reconnection job
2146 private void cancelReconnectJob() {
2147 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2148 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2149 reconnectJob.cancel(true);
2150 this.reconnectJob = null;
2155 * Update the state of a channel
2157 * @param channel the channel
2159 private void updateChannelState(String channel) {
2160 if (!isLinked(channel)) {
2163 State state = UnDefType.UNDEF;
2166 case CHANNEL_MAIN_POWER:
2169 state = OnOffType.from(po.booleanValue());
2172 case CHANNEL_ZONE2_POWER:
2173 state = OnOffType.from(powerZone2);
2175 case CHANNEL_ZONE3_POWER:
2176 state = OnOffType.from(powerZone3);
2178 case CHANNEL_ZONE4_POWER:
2179 state = OnOffType.from(powerZone4);
2181 case CHANNEL_SOURCE:
2182 case CHANNEL_MAIN_SOURCE:
2184 state = new StringType(source.getName());
2187 case CHANNEL_MAIN_RECORD_SOURCE:
2188 RotelSource recordSource = this.recordSource;
2189 if (isPowerOn() && recordSource != null) {
2190 state = new StringType(recordSource.getName());
2193 case CHANNEL_ZONE2_SOURCE:
2194 RotelSource sourceZone2 = this.sourceZone2;
2195 if (powerZone2 && sourceZone2 != null) {
2196 state = new StringType(sourceZone2.getName());
2199 case CHANNEL_ZONE3_SOURCE:
2200 RotelSource sourceZone3 = this.sourceZone3;
2201 if (powerZone3 && sourceZone3 != null) {
2202 state = new StringType(sourceZone3.getName());
2205 case CHANNEL_ZONE4_SOURCE:
2206 RotelSource sourceZone4 = this.sourceZone4;
2207 if (powerZone4 && sourceZone4 != null) {
2208 state = new StringType(sourceZone4.getName());
2212 case CHANNEL_MAIN_DSP:
2214 state = new StringType(dsp.getName());
2217 case CHANNEL_VOLUME:
2218 case CHANNEL_MAIN_VOLUME:
2220 long volumePct = Math
2221 .round((double) (volume - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2222 state = new PercentType(BigDecimal.valueOf(volumePct));
2225 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2227 state = new DecimalType(volume);
2230 case CHANNEL_ZONE2_VOLUME:
2231 if (powerZone2 && !fixedVolumeZone2) {
2232 long volumePct = Math
2233 .round((double) (volumeZone2 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2234 state = new PercentType(BigDecimal.valueOf(volumePct));
2237 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2238 if (powerZone2 && !fixedVolumeZone2) {
2239 state = new DecimalType(volumeZone2);
2242 case CHANNEL_ZONE3_VOLUME:
2243 if (powerZone3 && !fixedVolumeZone3) {
2244 long volumePct = Math
2245 .round((double) (volumeZone3 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2246 state = new PercentType(BigDecimal.valueOf(volumePct));
2249 case CHANNEL_ZONE4_VOLUME:
2250 if (powerZone4 && !fixedVolumeZone4) {
2251 long volumePct = Math
2252 .round((double) (volumeZone4 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2253 state = new PercentType(BigDecimal.valueOf(volumePct));
2257 case CHANNEL_MAIN_MUTE:
2259 state = OnOffType.from(mute);
2262 case CHANNEL_ZONE2_MUTE:
2264 state = OnOffType.from(muteZone2);
2267 case CHANNEL_ZONE3_MUTE:
2269 state = OnOffType.from(muteZone3);
2272 case CHANNEL_ZONE4_MUTE:
2274 state = OnOffType.from(muteZone4);
2278 case CHANNEL_MAIN_BASS:
2280 state = new DecimalType(bass);
2283 case CHANNEL_TREBLE:
2284 case CHANNEL_MAIN_TREBLE:
2286 state = new DecimalType(treble);
2290 if (track > 0 && isPowerOn()) {
2291 state = new DecimalType(track);
2294 case CHANNEL_PLAY_CONTROL:
2296 switch (playStatus) {
2298 state = PlayPauseType.PLAY;
2302 state = PlayPauseType.PAUSE;
2307 case CHANNEL_FREQUENCY:
2308 if (frequency > 0.0 && isPowerOn()) {
2309 state = new DecimalType(frequency);
2313 state = new StringType(frontPanelLine1);
2316 state = new StringType(frontPanelLine2);
2318 case CHANNEL_BRIGHTNESS:
2319 if (isPowerOn() && model.hasDimmerControl()) {
2320 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2321 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2322 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2325 case CHANNEL_TCBYPASS:
2327 state = OnOffType.from(tcbypass);
2330 case CHANNEL_BALANCE:
2332 state = new DecimalType(balance);
2335 case CHANNEL_SPEAKER_A:
2337 state = OnOffType.from(speakera);
2340 case CHANNEL_SPEAKER_B:
2342 state = OnOffType.from(speakerb);
2348 updateState(channel, state);
2352 * Inform about the main zone power state
2354 * @return true if main zone power state is known and known as ON
2356 private boolean isPowerOn() {
2357 Boolean power = this.power;
2358 return power != null && power.booleanValue();
2362 * Get the command to be used for main zone POWER ON
2364 * @return the command
2366 private RotelCommand getPowerOnCommand() {
2367 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2371 * Get the command to be used for main zone POWER OFF
2373 * @return the command
2375 private RotelCommand getPowerOffCommand() {
2376 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2380 * Get the command to be used for main zone VOLUME UP
2382 * @return the command
2384 private RotelCommand getVolumeUpCommand() {
2385 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2389 * Get the command to be used for main zone VOLUME DOWN
2391 * @return the command
2393 private RotelCommand getVolumeDownCommand() {
2394 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN : RotelCommand.VOLUME_DOWN;
2398 * Get the command to be used for main zone MUTE ON
2400 * @return the command
2402 private RotelCommand getMuteOnCommand() {
2403 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2407 * Get the command to be used for main zone MUTE OFF
2409 * @return the command
2411 private RotelCommand getMuteOffCommand() {
2412 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2416 * Get the command to be used for main zone MUTE TOGGLE
2418 * @return the command
2420 private RotelCommand getMuteToggleCommand() {
2421 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE : RotelCommand.MUTE_TOGGLE;
2424 private void sendCommand(RotelCommand cmd) throws RotelException {
2425 sendCommand(cmd, null);
2429 * Request the Rotel device to execute a command
2431 * @param cmd the command to execute
2432 * @param value the integer value to consider for volume, bass or treble adjustment
2434 * @throws RotelException - In case of any problem
2436 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2439 message = protocolHandler.buildCommandMessage(cmd, value);
2440 } catch (RotelException e) {
2441 // Command not supported
2442 logger.debug("sendCommand: {}", e.getMessage());
2445 connector.writeOutput(cmd.getName(), message);
2447 if (connector instanceof RotelSimuConnector) {
2448 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2449 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2450 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2451 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);