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.RotelCommandDescriptionOptionProvider;
30 import org.openhab.binding.rotel.internal.RotelException;
31 import org.openhab.binding.rotel.internal.RotelModel;
32 import org.openhab.binding.rotel.internal.RotelPlayStatus;
33 import org.openhab.binding.rotel.internal.RotelRepeatMode;
34 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
35 import org.openhab.binding.rotel.internal.communication.RotelCommand;
36 import org.openhab.binding.rotel.internal.communication.RotelConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelDsp;
38 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
40 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
41 import org.openhab.binding.rotel.internal.communication.RotelSource;
42 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
43 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
44 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
45 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
46 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
47 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
48 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
49 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
50 import org.openhab.core.io.transport.serial.SerialPortManager;
51 import org.openhab.core.library.types.DecimalType;
52 import org.openhab.core.library.types.IncreaseDecreaseType;
53 import org.openhab.core.library.types.NextPreviousType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.PercentType;
56 import org.openhab.core.library.types.PlayPauseType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.BaseThingHandler;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.CommandOption;
65 import org.openhab.core.types.RefreshType;
66 import org.openhab.core.types.State;
67 import org.openhab.core.types.StateOption;
68 import org.openhab.core.types.UnDefType;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
73 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
75 * @author Laurent Garnier - Initial contribution
78 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
80 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
82 private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
83 private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
84 private static final boolean USE_SIMULATED_DEVICE = false;
85 private static final int SLEEP_INTV = 30;
87 private final RotelStateDescriptionOptionProvider stateDescriptionProvider;
88 private final RotelCommandDescriptionOptionProvider commandDescriptionProvider;
89 private final SerialPortManager serialPortManager;
91 private @Nullable ScheduledFuture<?> reconnectJob;
92 private @Nullable ScheduledFuture<?> powerOffJob;
93 private @Nullable ScheduledFuture<?>[] powerOnZoneJobs = { null, null, null, null, null };
95 private RotelModel model;
96 private RotelProtocol protocol;
97 private RotelAbstractProtocolHandler protocolHandler;
98 private RotelConnector connector;
100 private int minVolume;
101 private int maxVolume;
102 private int minToneLevel;
103 private int maxToneLevel;
105 private int currentZone = 1;
106 private boolean selectingRecord;
107 private @Nullable Boolean[] powers = { null, false, false, false, false };
108 private boolean powerControlPerZone;
109 private @Nullable RotelSource recordSource;
110 private @Nullable RotelSource[] sources = { RotelSource.CAT0_CD, null, null, null, null };
111 private RotelDsp dsp = RotelDsp.CAT1_NONE;
112 private boolean[] fixedVolumeZones = { false, false, false, false, false };
113 private int[] volumes = { 0, 0, 0, 0, 0 };
114 private boolean[] mutes = { false, false, false, false, false };
115 private int[] basses = { 0, 0, 0, 0, 0 };
116 private int[] trebles = { 0, 0, 0, 0, 0 };
117 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
119 private boolean randomMode;
120 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
121 private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
122 private String frontPanelLine1 = "";
123 private String frontPanelLine2 = "";
124 private int brightness;
125 private boolean tcbypass;
126 private int[] balances = { 0, 0, 0, 0, 0 };
127 private int minBalanceLevel;
128 private int maxBalanceLevel;
129 private boolean speakera;
130 private boolean speakerb;
132 private Object sequenceLock = new Object();
137 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
138 RotelCommandDescriptionOptionProvider commandDescriptionProvider, SerialPortManager serialPortManager) {
140 this.stateDescriptionProvider = stateDescriptionProvider;
141 this.commandDescriptionProvider = commandDescriptionProvider;
142 this.serialPortManager = serialPortManager;
143 this.model = DEFAULT_MODEL;
144 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
145 this.protocol = protocolHandler.getProtocol();
146 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
150 public void initialize() {
151 logger.debug("Start initializing handler for thing {}", getThing().getUID());
153 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
155 protocol = RotelProtocol.HEX;
156 if (config.protocol != null && !config.protocol.isEmpty()) {
158 protocol = RotelProtocol.getFromName(config.protocol);
159 } catch (RotelException e) {
160 // Invalid protocol name in configuration, HEX will be considered by default
163 Map<String, String> properties = editProperties();
164 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
165 if (property != null && !property.isEmpty()) {
167 protocol = RotelProtocol.getFromName(property);
168 } catch (RotelException e) {
169 // Invalid protocol name in thing property, HEX will be considered by default
173 logger.debug("rotelProtocol {}", protocol.getName());
175 switch (getThing().getThingTypeUID().getId()) {
176 case THING_TYPE_ID_RSP1066:
177 model = RotelModel.RSP1066;
179 case THING_TYPE_ID_RSP1068:
180 model = RotelModel.RSP1068;
182 case THING_TYPE_ID_RSP1069:
183 model = RotelModel.RSP1069;
185 case THING_TYPE_ID_RSP1098:
186 model = RotelModel.RSP1098;
188 case THING_TYPE_ID_RSP1570:
189 model = RotelModel.RSP1570;
191 case THING_TYPE_ID_RSP1572:
192 model = RotelModel.RSP1572;
194 case THING_TYPE_ID_RSX1055:
195 model = RotelModel.RSX1055;
197 case THING_TYPE_ID_RSX1056:
198 model = RotelModel.RSX1056;
200 case THING_TYPE_ID_RSX1057:
201 model = RotelModel.RSX1057;
203 case THING_TYPE_ID_RSX1058:
204 model = RotelModel.RSX1058;
206 case THING_TYPE_ID_RSX1065:
207 model = RotelModel.RSX1065;
209 case THING_TYPE_ID_RSX1067:
210 model = RotelModel.RSX1067;
212 case THING_TYPE_ID_RSX1550:
213 model = RotelModel.RSX1550;
215 case THING_TYPE_ID_RSX1560:
216 model = RotelModel.RSX1560;
218 case THING_TYPE_ID_RSX1562:
219 model = RotelModel.RSX1562;
221 case THING_TYPE_ID_A11:
222 model = RotelModel.A11;
224 case THING_TYPE_ID_A12:
225 model = RotelModel.A12;
227 case THING_TYPE_ID_A14:
228 model = RotelModel.A14;
230 case THING_TYPE_ID_CD11:
231 model = RotelModel.CD11;
233 case THING_TYPE_ID_CD14:
234 model = RotelModel.CD14;
236 case THING_TYPE_ID_RA11:
237 model = RotelModel.RA11;
239 case THING_TYPE_ID_RA12:
240 model = RotelModel.RA12;
242 case THING_TYPE_ID_RA1570:
243 model = RotelModel.RA1570;
245 case THING_TYPE_ID_RA1572:
246 model = RotelModel.RA1572;
248 case THING_TYPE_ID_RA1592:
249 if (protocol == RotelProtocol.ASCII_V1) {
250 model = RotelModel.RA1592_V1;
252 model = RotelModel.RA1592_V2;
255 case THING_TYPE_ID_RAP1580:
256 model = RotelModel.RAP1580;
258 case THING_TYPE_ID_RC1570:
259 model = RotelModel.RC1570;
261 case THING_TYPE_ID_RC1572:
262 model = RotelModel.RC1572;
264 case THING_TYPE_ID_RC1590:
265 if (protocol == RotelProtocol.ASCII_V1) {
266 model = RotelModel.RC1590_V1;
268 model = RotelModel.RC1590_V2;
271 case THING_TYPE_ID_RCD1570:
272 model = RotelModel.RCD1570;
274 case THING_TYPE_ID_RCD1572:
275 model = RotelModel.RCD1572;
277 case THING_TYPE_ID_RCX1500:
278 model = RotelModel.RCX1500;
280 case THING_TYPE_ID_RDD1580:
281 model = RotelModel.RDD1580;
283 case THING_TYPE_ID_RDG1520:
284 case THING_TYPE_ID_RT09:
285 model = RotelModel.RDG1520;
287 case THING_TYPE_ID_RSP1576:
288 model = RotelModel.RSP1576;
290 case THING_TYPE_ID_RSP1582:
291 model = RotelModel.RSP1582;
293 case THING_TYPE_ID_RT11:
294 model = RotelModel.RT11;
296 case THING_TYPE_ID_RT1570:
297 model = RotelModel.RT1570;
299 case THING_TYPE_ID_T11:
300 model = RotelModel.T11;
302 case THING_TYPE_ID_T14:
303 model = RotelModel.T14;
305 case THING_TYPE_ID_C8:
306 model = RotelModel.C8;
308 case THING_TYPE_ID_M8:
309 model = RotelModel.M8;
311 case THING_TYPE_ID_P5:
312 model = RotelModel.P5;
314 case THING_TYPE_ID_S5:
315 model = RotelModel.S5;
317 case THING_TYPE_ID_X3:
318 model = RotelModel.X3;
320 case THING_TYPE_ID_X5:
321 model = RotelModel.X5;
324 model = DEFAULT_MODEL;
328 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
329 Map<RotelSource, String> sourcesLabels = new HashMap<>();
331 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
333 if (model.hasVolumeControl()) {
334 maxVolume = model.getVolumeMax();
335 if (!model.hasDirectVolumeControl()) {
337 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
338 minVolume, maxVolume);
341 if (model.hasToneControl()) {
342 maxToneLevel = model.getToneLevelMax();
343 minToneLevel = -maxToneLevel;
345 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
346 minToneLevel, maxToneLevel);
348 if (model.hasBalanceControl()) {
349 maxBalanceLevel = model.getBalanceLevelMax();
350 minBalanceLevel = -maxBalanceLevel;
351 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
352 minBalanceLevel, maxBalanceLevel);
355 powerControlPerZone = model.hasPowerControlPerZone();
357 // Check configuration settings
358 String configError = null;
359 if ((config.serialPort == null || config.serialPort.isEmpty())
360 && (config.host == null || config.host.isEmpty())) {
361 configError = "@text/offline.config-error-unknown-serialport-and-host";
362 } else if (config.host == null || config.host.isEmpty()) {
363 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
364 configError = "@text/offline.config-error-invalid-serial-over-ip";
367 if (config.port == null) {
368 configError = "@text/offline.config-error-unknown-port";
369 } else if (config.port <= 0) {
370 configError = "@text/offline.config-error-invalid-port";
374 if (configError != null) {
375 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
377 for (RotelSource src : model.getSources()) {
378 // Consider custom input labels
380 switch (src.getName()) {
382 label = config.inputLabelCd;
385 label = config.inputLabelTuner;
388 label = config.inputLabelTape;
391 label = config.inputLabelPhono;
394 label = config.inputLabelVideo1;
397 label = config.inputLabelVideo2;
400 label = config.inputLabelVideo3;
403 label = config.inputLabelVideo4;
406 label = config.inputLabelVideo5;
409 label = config.inputLabelVideo6;
412 label = config.inputLabelUsb;
415 label = config.inputLabelMulti;
420 if (label != null && !label.isEmpty()) {
421 sourcesCustomLabels.put(src, label);
423 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
426 if (protocol == RotelProtocol.HEX) {
427 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
428 } else if (protocol == RotelProtocol.ASCII_V1) {
429 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
431 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
434 if (USE_SIMULATED_DEVICE) {
435 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
436 } else if (config.serialPort != null) {
437 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
438 protocolHandler, readerThreadName);
440 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
443 if (model.hasSourceControl()) {
444 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
445 getStateOptions(model.getSources(), sourcesCustomLabels));
446 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
447 getStateOptions(model.getSources(), sourcesCustomLabels));
448 stateDescriptionProvider.setStateOptions(
449 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
450 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
452 if (model.hasZoneSourceControl(1)) {
453 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
454 getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
456 if (model.hasZoneSourceControl(2)) {
457 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
458 getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
460 if (model.hasZoneSourceControl(3)) {
461 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
462 getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
464 if (model.hasZoneSourceControl(4)) {
465 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
466 getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
468 if (model.hasDspControl()) {
469 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
470 model.getDspStateOptions());
471 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
472 model.getDspStateOptions());
475 List<CommandOption> options = model.getOtherCommandsOptions(protocol);
476 if (!options.isEmpty()) {
477 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_OTHER_COMMAND),
479 commandDescriptionProvider
480 .setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_OTHER_COMMAND), options);
483 updateStatus(ThingStatus.UNKNOWN);
485 scheduleReconnectJob();
488 logger.debug("Finished initializing!");
492 public void dispose() {
493 logger.debug("Disposing handler for thing {}", getThing().getUID());
495 for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
496 cancelPowerOnZoneJob(zone);
498 cancelReconnectJob();
503 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
504 List<StateOption> options = new ArrayList<>();
505 for (RotelSource item : list) {
506 String label = sourcesLabels.get(item);
507 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
513 public void handleCommand(ChannelUID channelUID, Command command) {
514 String channel = channelUID.getId();
516 if (getThing().getStatus() != ThingStatus.ONLINE) {
517 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
521 if (command instanceof RefreshType) {
522 updateChannelState(channel);
526 if (!connector.isConnected()) {
527 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
533 case CHANNEL_ZONE1_SOURCE:
534 case CHANNEL_ZONE1_VOLUME:
535 case CHANNEL_ZONE1_MUTE:
536 case CHANNEL_ZONE1_BASS:
537 case CHANNEL_ZONE1_TREBLE:
538 case CHANNEL_ZONE1_BALANCE:
541 case CHANNEL_ZONE2_POWER:
542 case CHANNEL_ZONE2_SOURCE:
543 case CHANNEL_ZONE2_VOLUME:
544 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
545 case CHANNEL_ZONE2_MUTE:
546 case CHANNEL_ZONE2_BASS:
547 case CHANNEL_ZONE2_TREBLE:
548 case CHANNEL_ZONE2_BALANCE:
551 case CHANNEL_ZONE3_POWER:
552 case CHANNEL_ZONE3_SOURCE:
553 case CHANNEL_ZONE3_VOLUME:
554 case CHANNEL_ZONE3_MUTE:
555 case CHANNEL_ZONE3_BASS:
556 case CHANNEL_ZONE3_TREBLE:
557 case CHANNEL_ZONE3_BALANCE:
560 case CHANNEL_ZONE4_POWER:
561 case CHANNEL_ZONE4_SOURCE:
562 case CHANNEL_ZONE4_VOLUME:
563 case CHANNEL_ZONE4_MUTE:
564 case CHANNEL_ZONE4_BASS:
565 case CHANNEL_ZONE4_TREBLE:
566 case CHANNEL_ZONE4_BALANCE:
575 boolean success = true;
576 synchronized (sequenceLock) {
580 case CHANNEL_MAIN_POWER:
581 case CHANNEL_ZONE2_POWER:
582 case CHANNEL_ZONE3_POWER:
583 case CHANNEL_ZONE4_POWER:
584 if (numZone == 0 || model.hasZoneCommands(numZone)) {
585 handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
586 } else if (numZone == 2 && model.getNumberOfZones() == 2) {
587 if (isPowerOn() || isPowerOn(numZone)) {
588 selectZone(2, model.getZoneSelectCmd());
590 sendCommand(RotelCommand.ZONE_SELECT);
593 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
596 case CHANNEL_ALL_POWER:
597 handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
600 case CHANNEL_MAIN_SOURCE:
601 case CHANNEL_ZONE1_SOURCE:
602 case CHANNEL_ZONE2_SOURCE:
603 case CHANNEL_ZONE3_SOURCE:
604 case CHANNEL_ZONE4_SOURCE:
605 if (!isPowerOn(numZone)) {
607 logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
608 numZone == 0 ? "device" : "zone " + numZone);
609 } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
610 src = model.getSourceFromName(command.toString());
612 cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
614 cmd = src.getZoneCommand(numZone);
618 if (model.canGetFrequency()) {
619 // send <new-source> returns
620 // 1.) the selected <new-source>
621 // 2.) the used frequency
623 // at response-time the frequency has the value of <old-source>
624 // so we must wait a short moment to get the frequency of <new-source>
626 sendCommand(RotelCommand.FREQUENCY);
628 updateChannelState(CHANNEL_FREQUENCY);
632 logger.debug("Command {} from channel {} failed: undefined source command", command,
635 } else if (numZone == 2 && model.getNumberOfZones() > 1) {
636 src = model.getSourceFromName(command.toString());
637 cmd = src.getCommand();
639 selectZone(2, model.getZoneSelectCmd());
641 if (model.canGetFrequency()) {
642 // send <new-source> returns
643 // 1.) the selected <new-source>
644 // 2.) the used frequency
646 // at response-time the frequency has the value of <old-source>
647 // so we must wait a short moment to get the frequency of <new-source>
649 sendCommand(RotelCommand.FREQUENCY);
651 updateChannelState(CHANNEL_FREQUENCY);
655 logger.debug("Command {} from channel {} failed: undefined source command", command,
660 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
663 case CHANNEL_MAIN_RECORD_SOURCE:
666 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
667 } else if (model.hasOtherThanPrimaryCommands()) {
668 src = model.getSourceFromName(command.toString());
669 cmd = src.getRecordCommand();
674 logger.debug("Command {} from channel {} failed: undefined record source command",
678 src = model.getSourceFromName(command.toString());
679 cmd = src.getCommand();
681 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
686 logger.debug("Command {} from channel {} failed: undefined source command", command,
692 case CHANNEL_MAIN_DSP:
695 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
697 sendCommand(model.getCommandFromDspName(command.toString()));
701 case CHANNEL_MAIN_VOLUME:
702 case CHANNEL_MAIN_VOLUME_UP_DOWN:
703 case CHANNEL_ZONE1_VOLUME:
704 case CHANNEL_ZONE2_VOLUME:
705 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
706 case CHANNEL_ZONE3_VOLUME:
707 case CHANNEL_ZONE4_VOLUME:
708 if (!isPowerOn(numZone)) {
710 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
711 numZone == 0 ? "device" : "zone " + numZone);
712 } else if (fixedVolumeZones[numZone]) {
714 logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
715 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
716 handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
717 getVolumeDownCommand(numZone),
718 CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
719 || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
720 : getVolumeSetCommand(numZone));
721 } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
722 selectZone(2, model.getZoneSelectCmd());
723 handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
724 RotelCommand.VOLUME_DOWN,
725 CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
728 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
732 case CHANNEL_MAIN_MUTE:
733 case CHANNEL_ZONE1_MUTE:
734 case CHANNEL_ZONE2_MUTE:
735 case CHANNEL_ZONE3_MUTE:
736 case CHANNEL_ZONE4_MUTE:
737 if (!isPowerOn(numZone)) {
739 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
740 numZone == 0 ? "device" : "zone " + numZone);
741 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
742 handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
743 getMuteOnCommand(numZone), getMuteOffCommand(numZone),
744 getMuteToggleCommand(numZone));
747 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
751 case CHANNEL_MAIN_BASS:
752 case CHANNEL_ZONE1_BASS:
753 case CHANNEL_ZONE2_BASS:
754 case CHANNEL_ZONE3_BASS:
755 case CHANNEL_ZONE4_BASS:
756 if (!isPowerOn(numZone)) {
758 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
759 numZone == 0 ? "device" : "zone " + numZone);
760 } else if (tcbypass) {
762 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
764 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
765 handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
766 getBassDownCommand(numZone), getBassSetCommand(numZone));
769 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
773 case CHANNEL_MAIN_TREBLE:
774 case CHANNEL_ZONE1_TREBLE:
775 case CHANNEL_ZONE2_TREBLE:
776 case CHANNEL_ZONE3_TREBLE:
777 case CHANNEL_ZONE4_TREBLE:
778 if (!isPowerOn(numZone)) {
780 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
781 numZone == 0 ? "device" : "zone " + numZone);
782 } else if (tcbypass) {
784 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
786 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
787 handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
788 getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
791 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
794 case CHANNEL_PLAY_CONTROL:
797 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
798 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
799 sendCommand(RotelCommand.PLAY);
800 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
801 sendCommand(RotelCommand.PAUSE);
802 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
803 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
804 Thread.sleep(SLEEP_INTV);
805 sendCommand(RotelCommand.PLAY_STATUS);
807 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
808 sendCommand(RotelCommand.TRACK_FWD);
809 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
810 sendCommand(RotelCommand.TRACK_BACK);
813 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
819 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
820 } else if (command instanceof OnOffType) {
821 sendCommand(RotelCommand.RANDOM_TOGGLE);
824 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
830 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
832 RotelRepeatMode currentMode = repeatMode;
833 RotelRepeatMode mode = RotelRepeatMode.OFF;
835 mode = RotelRepeatMode.getFromName(command.toString());
836 if (mode == currentMode) {
838 logger.debug("Command {} from channel {} ignored: no change requested", command,
841 } catch (RotelException e) {
843 logger.debug("Command {} from channel {} failed: invalid command value", command,
847 // Toggle TRACK -> DISC -> OFF
848 sendCommand(RotelCommand.REPEAT_TOGGLE);
849 if ((mode == RotelRepeatMode.OFF && currentMode == RotelRepeatMode.TRACK)
850 || (mode == RotelRepeatMode.TRACK && currentMode == RotelRepeatMode.DISC)
851 || (mode == RotelRepeatMode.DISC && currentMode == RotelRepeatMode.OFF)) {
852 Thread.sleep(SLEEP_INTV);
853 sendCommand(RotelCommand.REPEAT_TOGGLE);
858 case CHANNEL_BRIGHTNESS:
859 case CHANNEL_ALL_BRIGHTNESS:
862 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
863 } else if (!model.hasDimmerControl()) {
865 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
866 } else if (command instanceof PercentType) {
867 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
868 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
869 + model.getDimmerLevelMin();
870 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
873 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
876 case CHANNEL_TCBYPASS:
879 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
880 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
882 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
884 handleTcbypassCmd(channel, command,
885 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
886 : RotelCommand.TCBYPASS_ON,
887 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
888 : RotelCommand.TCBYPASS_OFF);
891 case CHANNEL_BALANCE:
892 case CHANNEL_ZONE1_BALANCE:
893 case CHANNEL_ZONE2_BALANCE:
894 case CHANNEL_ZONE3_BALANCE:
895 case CHANNEL_ZONE4_BALANCE:
896 if (!isPowerOn(numZone)) {
898 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
899 numZone == 0 ? "device" : "zone " + numZone);
900 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
902 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
904 handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
905 getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
908 case CHANNEL_SPEAKER_A:
911 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
913 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
914 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
917 case CHANNEL_SPEAKER_B:
920 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
922 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
923 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
926 case CHANNEL_OTHER_COMMAND:
927 case CHANNEL_MAIN_OTHER_COMMAND:
930 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
933 cmd = RotelCommand.getFromName(command.toString());
934 } catch (RotelException e) {
936 logger.debug("Command {} from channel {} failed: undefined command", command, channel);
946 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
950 logger.debug("Command {} from channel {} succeeded", command, channel);
952 updateChannelState(channel);
954 } catch (RotelException e) {
955 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
956 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
957 "@text/offline.comm-error-sending-command");
959 scheduleReconnectJob();
960 } catch (InterruptedException e) {
961 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
962 Thread.currentThread().interrupt();
968 * Handle a power ON/OFF command
970 * @param channel the channel
971 * @param command the received channel command (OnOffType)
972 * @param onCmd the command to be sent to the device to power it ON
973 * @param offCmd the command to be sent to the device to power it OFF
975 * @throws RotelException in case of communication error with the device
977 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
978 throws RotelException {
979 if (command instanceof OnOffType && command == OnOffType.ON) {
981 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
984 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
989 * Handle a volume command
991 * @param current the current volume
992 * @param channel the channel
993 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
994 * @param upCmd the command to be sent to the device to increase the volume
995 * @param downCmd the command to be sent to the device to decrease the volume
996 * @param setCmd the command to be sent to the device to set the volume at a value
998 * @throws RotelException in case of communication error with the device
1000 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
1001 @Nullable RotelCommand setCmd) throws RotelException {
1002 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1004 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1005 sendCommand(downCmd);
1006 } else if (command instanceof DecimalType && setCmd == null) {
1007 int value = ((DecimalType) command).intValue();
1008 if (value >= minVolume && value <= maxVolume) {
1009 if (value > current) {
1011 } else if (value < current) {
1012 sendCommand(downCmd);
1015 } else if (command instanceof PercentType && setCmd != null) {
1016 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1018 sendCommand(setCmd, value);
1020 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1025 * Handle a mute command
1027 * @param onlyToggle true if only the toggle command must be used
1028 * @param channel the channel
1029 * @param command the received channel command (OnOffType)
1030 * @param onCmd the command to be sent to the device to mute
1031 * @param offCmd the command to be sent to the device to unmute
1032 * @param toggleCmd the command to be sent to the device to toggle the mute state
1034 * @throws RotelException in case of communication error with the device
1036 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1037 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1038 if (command instanceof OnOffType) {
1040 sendCommand(toggleCmd);
1041 } else if (command == OnOffType.ON) {
1043 } else if (command == OnOffType.OFF) {
1044 sendCommand(offCmd);
1047 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1052 * Handle a tone level adjustment command (bass or treble)
1054 * @param current the current tone level
1055 * @param channel the channel
1056 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1057 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1058 * @param upCmd the command to be sent to the device to increase the tone level
1059 * @param downCmd the command to be sent to the device to decrease the tone level
1060 * @param setCmd the command to be sent to the device to set the tone level at a value
1062 * @throws RotelException in case of communication error with the device
1063 * @throws InterruptedException in case of interruption during a thread sleep
1065 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1066 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1067 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1068 selectToneControl(nbSelect);
1070 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1071 selectToneControl(nbSelect);
1072 sendCommand(downCmd);
1073 } else if (command instanceof DecimalType) {
1074 int value = ((DecimalType) command).intValue();
1075 if (value >= minToneLevel && value <= maxToneLevel) {
1076 if (protocol != RotelProtocol.HEX) {
1077 sendCommand(setCmd, value);
1078 } else if (value > current) {
1079 selectToneControl(nbSelect);
1081 } else if (value < current) {
1082 selectToneControl(nbSelect);
1083 sendCommand(downCmd);
1087 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1092 * Handle a tcbypass command (only for ASCII protocol)
1094 * @param channel the channel
1095 * @param command the received channel command (OnOffType)
1096 * @param onCmd the command to be sent to the device to bypass_on
1097 * @param offCmd the command to be sent to the device to bypass_off
1099 * @throws RotelException in case of communication error with the device
1101 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1102 throws RotelException, InterruptedException {
1103 if (command instanceof OnOffType) {
1104 if (command == OnOffType.ON) {
1108 updateChannelState(CHANNEL_BASS);
1109 updateChannelState(CHANNEL_TREBLE);
1110 } else if (command == OnOffType.OFF) {
1111 sendCommand(offCmd);
1113 sendCommand(RotelCommand.BASS);
1115 sendCommand(RotelCommand.TREBLE);
1118 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1123 * Handle a speaker command
1125 * @param onlyToggle true if only the toggle command must be used
1126 * @param channel the channel
1127 * @param command the received channel command (OnOffType)
1128 * @param onCmd the command to be sent to the device to speaker_x_on
1129 * @param offCmd the command to be sent to the device to speaker_x_off
1130 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1132 * @throws RotelException in case of communication error with the device
1134 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1135 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1136 if (command instanceof OnOffType) {
1138 sendCommand(toggleCmd);
1139 } else if (command == OnOffType.ON) {
1141 } else if (command == OnOffType.OFF) {
1142 sendCommand(offCmd);
1145 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1150 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1152 * @param channel the channel
1153 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1154 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1155 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1156 * @param setCmd the command to be sent to the device to set the balance at a value
1158 * @throws RotelException in case of communication error with the device
1159 * @throws InterruptedException in case of interruption during a thread sleep
1161 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1162 RotelCommand setCmd) throws RotelException, InterruptedException {
1163 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1164 sendCommand(rightCmd);
1165 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1166 sendCommand(leftCmd);
1167 } else if (command instanceof DecimalType) {
1168 int value = ((DecimalType) command).intValue();
1169 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1170 sendCommand(setCmd, value);
1173 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1178 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1180 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1182 * @throws RotelException in case of communication error with the device
1183 * @throws InterruptedException in case of interruption during a thread sleep
1185 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1186 // No tone control select command for RSX-1065
1187 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1188 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1193 * Run a sequence of commands to display a particular zone on the device front panel
1195 * @param zone the zone to be displayed (1 for main zone)
1196 * @param selectCommand the command to be sent to the device to switch the display between zones
1198 * @throws RotelException in case of communication error with the device
1199 * @throws InterruptedException in case of interruption during a thread sleep
1201 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1202 throws RotelException, InterruptedException {
1203 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
1204 && selectCommand != null) {
1206 if (zone < currentZone) {
1207 nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
1208 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1212 nbSelect = zone - currentZone;
1213 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1214 && !selectingRecord) {
1218 selectFeature(nbSelect, null, selectCommand);
1223 * Run a sequence of commands to display a particular feature on the device front panel
1225 * @param nbSelect the number of select commands to be run
1226 * @param preCmd the initial command to be sent to the device (before the select commands)
1227 * @param selectCmd the select command to be sent to the device
1229 * @throws RotelException in case of communication error with the device
1230 * @throws InterruptedException in case of interruption during a thread sleep
1232 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1233 throws RotelException, InterruptedException {
1234 if (protocol == RotelProtocol.HEX) {
1235 if (preCmd != null) {
1236 sendCommand(preCmd);
1239 for (int i = 1; i <= nbSelect; i++) {
1240 sendCommand(selectCmd);
1247 * Open the connection with the Rotel device
1249 * @return true if the connection is opened successfully or flase if not
1251 private synchronized boolean openConnection() {
1252 protocolHandler.addEventListener(this);
1255 } catch (RotelException e) {
1256 logger.debug("openConnection() failed", e);
1258 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1259 return connector.isConnected();
1263 * Close the connection with the Rotel device
1265 private synchronized void closeConnection() {
1267 protocolHandler.removeEventListener(this);
1268 logger.debug("closeConnection(): disconnected");
1272 public void onNewMessageEvent(EventObject event) {
1273 cancelPowerOffJob();
1275 RotelMessageEvent evt = (RotelMessageEvent) event;
1276 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1278 String key = evt.getKey();
1279 String value = evt.getValue().trim();
1280 if (!KEY_ERROR.equals(key)) {
1281 updateStatus(ThingStatus.ONLINE);
1285 case KEY_INPUT_ZONE1:
1286 case KEY_VOLUME_ZONE1:
1287 case KEY_MUTE_ZONE1:
1288 case KEY_BASS_ZONE1:
1289 case KEY_TREBLE_ZONE1:
1290 case KEY_BALANCE_ZONE1:
1291 case KEY_FREQ_ZONE1:
1294 case KEY_POWER_ZONE2:
1295 case KEY_SOURCE_ZONE2:
1296 case KEY_INPUT_ZONE2:
1297 case KEY_VOLUME_ZONE2:
1298 case KEY_MUTE_ZONE2:
1299 case KEY_BASS_ZONE2:
1300 case KEY_TREBLE_ZONE2:
1301 case KEY_BALANCE_ZONE2:
1302 case KEY_FREQ_ZONE2:
1305 case KEY_POWER_ZONE3:
1306 case KEY_SOURCE_ZONE3:
1307 case KEY_INPUT_ZONE3:
1308 case KEY_VOLUME_ZONE3:
1309 case KEY_MUTE_ZONE3:
1310 case KEY_BASS_ZONE3:
1311 case KEY_TREBLE_ZONE3:
1312 case KEY_BALANCE_ZONE3:
1313 case KEY_FREQ_ZONE3:
1316 case KEY_POWER_ZONE4:
1317 case KEY_SOURCE_ZONE4:
1318 case KEY_INPUT_ZONE4:
1319 case KEY_VOLUME_ZONE4:
1320 case KEY_MUTE_ZONE4:
1321 case KEY_BASS_ZONE4:
1322 case KEY_TREBLE_ZONE4:
1323 case KEY_BALANCE_ZONE4:
1324 case KEY_FREQ_ZONE4:
1333 logger.debug("Reading feedback message failed");
1334 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1335 "@text/offline.comm-error-reading-thread");
1339 frontPanelLine1 = value;
1340 updateChannelState(CHANNEL_LINE1);
1343 frontPanelLine2 = value;
1344 updateChannelState(CHANNEL_LINE2);
1347 currentZone = Integer.parseInt(value);
1349 case KEY_RECORD_SEL:
1350 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1353 if (POWER_ON.equalsIgnoreCase(value)) {
1355 } else if (STANDBY.equalsIgnoreCase(value)) {
1357 if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
1358 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1359 handlePowerOffZone(zone);
1362 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1363 schedulePowerOffJob(false);
1365 throw new RotelException("Invalid value");
1368 case KEY_POWER_ZONE2:
1369 case KEY_POWER_ZONE3:
1370 case KEY_POWER_ZONE4:
1371 if (POWER_ON.equalsIgnoreCase(value)) {
1372 handlePowerOnZone(numZone);
1373 } else if (STANDBY.equalsIgnoreCase(value)) {
1374 handlePowerOffZone(numZone);
1376 throw new RotelException("Invalid value");
1379 case KEY_POWER_MODE:
1380 logger.debug("Power mode is set to {}", value);
1382 case KEY_VOLUME_MIN:
1383 minVolume = Integer.parseInt(value);
1384 if (!model.hasDirectVolumeControl()) {
1385 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1389 case KEY_VOLUME_MAX:
1390 maxVolume = Integer.parseInt(value);
1391 if (!model.hasDirectVolumeControl()) {
1392 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1397 case KEY_VOLUME_ZONE1:
1398 case KEY_VOLUME_ZONE2:
1399 case KEY_VOLUME_ZONE3:
1400 case KEY_VOLUME_ZONE4:
1401 fixedVolumeZones[numZone] = false;
1402 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1403 fixedVolumeZones[numZone] = true;
1404 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1405 volumes[numZone] = minVolume;
1406 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1407 volumes[numZone] = maxVolume;
1409 volumes[numZone] = Integer.parseInt(value);
1412 updateChannelState(CHANNEL_VOLUME);
1413 updateChannelState(CHANNEL_MAIN_VOLUME);
1414 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1416 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1417 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1421 case KEY_MUTE_ZONE1:
1422 case KEY_MUTE_ZONE2:
1423 case KEY_MUTE_ZONE3:
1424 case KEY_MUTE_ZONE4:
1425 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1426 mutes[numZone] = true;
1428 updateChannelState(CHANNEL_MUTE);
1429 updateChannelState(CHANNEL_MAIN_MUTE);
1431 updateGroupChannelState(numZone, CHANNEL_MUTE);
1433 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1434 mutes[numZone] = false;
1436 updateChannelState(CHANNEL_MUTE);
1437 updateChannelState(CHANNEL_MAIN_MUTE);
1439 updateGroupChannelState(numZone, CHANNEL_MUTE);
1442 throw new RotelException("Invalid value");
1446 maxToneLevel = Integer.parseInt(value);
1447 minToneLevel = -maxToneLevel;
1449 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1450 minToneLevel, maxToneLevel);
1453 case KEY_BASS_ZONE1:
1454 case KEY_BASS_ZONE2:
1455 case KEY_BASS_ZONE3:
1456 case KEY_BASS_ZONE4:
1457 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1458 basses[numZone] = minToneLevel;
1459 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1460 basses[numZone] = maxToneLevel;
1462 basses[numZone] = Integer.parseInt(value);
1465 updateChannelState(CHANNEL_BASS);
1466 updateChannelState(CHANNEL_MAIN_BASS);
1468 updateGroupChannelState(numZone, CHANNEL_BASS);
1472 case KEY_TREBLE_ZONE1:
1473 case KEY_TREBLE_ZONE2:
1474 case KEY_TREBLE_ZONE3:
1475 case KEY_TREBLE_ZONE4:
1476 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1477 trebles[numZone] = minToneLevel;
1478 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1479 trebles[numZone] = maxToneLevel;
1481 trebles[numZone] = Integer.parseInt(value);
1484 updateChannelState(CHANNEL_TREBLE);
1485 updateChannelState(CHANNEL_MAIN_TREBLE);
1487 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1491 sources[0] = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1492 updateChannelState(CHANNEL_SOURCE);
1493 updateChannelState(CHANNEL_MAIN_SOURCE);
1496 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1497 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1499 case KEY_SOURCE_ZONE2:
1500 case KEY_SOURCE_ZONE3:
1501 case KEY_SOURCE_ZONE4:
1502 case KEY_INPUT_ZONE1:
1503 case KEY_INPUT_ZONE2:
1504 case KEY_INPUT_ZONE3:
1505 case KEY_INPUT_ZONE4:
1506 sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
1507 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1510 if ("dolby_pliix_movie".equals(value)) {
1511 value = "dolby_plii_movie";
1512 } else if ("dolby_pliix_music".equals(value)) {
1513 value = "dolby_plii_music";
1514 } else if ("dolby_pliix_game".equals(value)) {
1515 value = "dolby_plii_game";
1517 dsp = model.getDspFromFeedback(value);
1518 logger.debug("DSP {}", dsp.getName());
1519 updateChannelState(CHANNEL_DSP);
1520 updateChannelState(CHANNEL_MAIN_DSP);
1522 case KEY1_PLAY_STATUS:
1523 case KEY2_PLAY_STATUS:
1524 if (PLAY.equalsIgnoreCase(value)) {
1525 playStatus = RotelPlayStatus.PLAYING;
1526 updateChannelState(CHANNEL_PLAY_CONTROL);
1527 } else if (PAUSE.equalsIgnoreCase(value)) {
1528 playStatus = RotelPlayStatus.PAUSED;
1529 updateChannelState(CHANNEL_PLAY_CONTROL);
1530 } else if (STOP.equalsIgnoreCase(value)) {
1531 playStatus = RotelPlayStatus.STOPPED;
1532 updateChannelState(CHANNEL_PLAY_CONTROL);
1534 throw new RotelException("Invalid value");
1538 RotelSource source = sources[0];
1539 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1540 track = Integer.parseInt(value);
1541 updateChannelState(CHANNEL_TRACK);
1545 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1547 updateChannelState(CHANNEL_RANDOM);
1548 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1550 updateChannelState(CHANNEL_RANDOM);
1552 throw new RotelException("Invalid value");
1556 if (TRACK.equalsIgnoreCase(value)) {
1557 repeatMode = RotelRepeatMode.TRACK;
1558 updateChannelState(CHANNEL_REPEAT);
1559 } else if (DISC.equalsIgnoreCase(value)) {
1560 repeatMode = RotelRepeatMode.DISC;
1561 updateChannelState(CHANNEL_REPEAT);
1562 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1563 repeatMode = RotelRepeatMode.OFF;
1564 updateChannelState(CHANNEL_REPEAT);
1566 throw new RotelException("Invalid value");
1570 case KEY_FREQ_ZONE1:
1571 case KEY_FREQ_ZONE2:
1572 case KEY_FREQ_ZONE3:
1573 case KEY_FREQ_ZONE4:
1574 if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
1575 frequencies[numZone] = 0.0;
1577 // Suppress a potential ending "k" or "K"
1578 if (value.toUpperCase().endsWith("K")) {
1579 value = value.substring(0, value.length() - 1);
1581 frequencies[numZone] = Double.parseDouble(value);
1584 updateChannelState(CHANNEL_FREQUENCY);
1586 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1590 brightness = Integer.parseInt(value);
1591 updateChannelState(CHANNEL_BRIGHTNESS);
1592 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1594 case KEY_UPDATE_MODE:
1595 case KEY_DISPLAY_UPDATE:
1598 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1600 updateChannelState(CHANNEL_TCBYPASS);
1601 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1603 updateChannelState(CHANNEL_TCBYPASS);
1605 throw new RotelException("Invalid value");
1609 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1611 updateChannelState(CHANNEL_TCBYPASS);
1612 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1614 updateChannelState(CHANNEL_TCBYPASS);
1616 throw new RotelException("Invalid value");
1620 case KEY_BALANCE_ZONE1:
1621 case KEY_BALANCE_ZONE2:
1622 case KEY_BALANCE_ZONE3:
1623 case KEY_BALANCE_ZONE4:
1624 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1625 balances[numZone] = minBalanceLevel;
1626 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1627 balances[numZone] = maxBalanceLevel;
1628 } else if (value.toUpperCase().startsWith("L")) {
1629 balances[numZone] = -Integer.parseInt(value.substring(1));
1630 } else if (value.toUpperCase().startsWith("R")) {
1631 balances[numZone] = Integer.parseInt(value.substring(1));
1633 balances[numZone] = Integer.parseInt(value);
1636 updateChannelState(CHANNEL_BALANCE);
1638 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1642 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1645 updateChannelState(CHANNEL_SPEAKER_A);
1646 updateChannelState(CHANNEL_SPEAKER_B);
1647 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1650 updateChannelState(CHANNEL_SPEAKER_A);
1651 updateChannelState(CHANNEL_SPEAKER_B);
1652 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1655 updateChannelState(CHANNEL_SPEAKER_A);
1656 updateChannelState(CHANNEL_SPEAKER_B);
1657 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1660 updateChannelState(CHANNEL_SPEAKER_A);
1661 updateChannelState(CHANNEL_SPEAKER_B);
1663 throw new RotelException("Invalid value");
1667 logger.debug("Sub level is set to {}", value);
1669 case KEY_CENTER_LEVEL:
1670 logger.debug("Center level is set to {}", value);
1672 case KEY_SURROUND_RIGHT_LEVEL:
1673 logger.debug("Surround right level is set to {}", value);
1675 case KEY_SURROUND_LEFT_LEVEL:
1676 logger.debug("Surround left level is set to {}", value);
1678 case KEY_CENTER_BACK_RIGHT_LEVEL:
1679 logger.debug("Center back right level is set to {}", value);
1681 case KEY_CENTER_BACK_LEFT_LEVEL:
1682 logger.debug("Center back left level is set to {}", value);
1684 case KEY_CEILING_FRONT_RIGHT_LEVEL:
1685 logger.debug("Ceiling front right level is set to {}", value);
1687 case KEY_CEILING_FRONT_LEFT_LEVEL:
1688 logger.debug("Ceiling front left level is set to {}", value);
1690 case KEY_CEILING_REAR_RIGHT_LEVEL:
1691 logger.debug("Ceiling rear right level is set to {}", value);
1693 case KEY_CEILING_REAR_LEFT_LEVEL:
1694 logger.debug("Ceiling rear left level is set to {}", value);
1696 case KEY_PCUSB_CLASS:
1697 logger.debug("PC-USB Audio Class is set to {}", value);
1699 case KEY_PRODUCT_TYPE:
1701 getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
1703 case KEY_PRODUCT_VERSION:
1705 getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
1708 logger.debug("onNewMessageEvent: unhandled key {}", key);
1711 } catch (NumberFormatException | RotelException e) {
1712 logger.debug("Invalid value {} for key {}", value, key);
1717 * Handle the received information that device power (main zone) is ON
1719 private void handlePowerOn() {
1720 Boolean prev = powers[0];
1722 updateChannelState(CHANNEL_POWER);
1723 updateChannelState(CHANNEL_MAIN_POWER);
1724 updateChannelState(CHANNEL_ALL_POWER);
1725 if ((prev == null) || !prev) {
1726 schedulePowerOnJob();
1731 * Handle the received information that device power (main zone) is OFF
1733 private void handlePowerOff() {
1734 cancelPowerOnZoneJob(0);
1736 updateChannelState(CHANNEL_POWER);
1737 updateChannelState(CHANNEL_SOURCE);
1738 updateChannelState(CHANNEL_DSP);
1739 updateChannelState(CHANNEL_VOLUME);
1740 updateChannelState(CHANNEL_MUTE);
1741 updateChannelState(CHANNEL_BASS);
1742 updateChannelState(CHANNEL_TREBLE);
1743 updateChannelState(CHANNEL_PLAY_CONTROL);
1744 updateChannelState(CHANNEL_TRACK);
1745 updateChannelState(CHANNEL_RANDOM);
1746 updateChannelState(CHANNEL_REPEAT);
1747 updateChannelState(CHANNEL_FREQUENCY);
1748 updateChannelState(CHANNEL_BRIGHTNESS);
1749 updateChannelState(CHANNEL_TCBYPASS);
1750 updateChannelState(CHANNEL_BALANCE);
1751 updateChannelState(CHANNEL_SPEAKER_A);
1752 updateChannelState(CHANNEL_SPEAKER_B);
1754 updateChannelState(CHANNEL_MAIN_POWER);
1755 updateChannelState(CHANNEL_MAIN_SOURCE);
1756 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1757 updateChannelState(CHANNEL_MAIN_DSP);
1758 updateChannelState(CHANNEL_MAIN_VOLUME);
1759 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1760 updateChannelState(CHANNEL_MAIN_MUTE);
1761 updateChannelState(CHANNEL_MAIN_BASS);
1762 updateChannelState(CHANNEL_MAIN_TREBLE);
1764 updateChannelState(CHANNEL_ALL_POWER);
1765 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1769 * Handle the received information that a zone power is ON
1771 private void handlePowerOnZone(int numZone) {
1772 Boolean prev = powers[numZone];
1773 powers[numZone] = true;
1774 updateGroupChannelState(numZone, CHANNEL_POWER);
1775 if ((prev == null) || !prev) {
1776 schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
1781 * Handle the received information that a zone power is OFF
1783 private void handlePowerOffZone(int numZone) {
1784 cancelPowerOnZoneJob(numZone);
1785 powers[numZone] = false;
1786 updateGroupChannelState(numZone, CHANNEL_POWER);
1787 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1788 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1789 updateGroupChannelState(numZone, CHANNEL_MUTE);
1790 updateGroupChannelState(numZone, CHANNEL_BASS);
1791 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1792 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1793 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1794 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1798 * Schedule the job that will consider the device as OFF if no new event is received before its running
1800 * @param switchOffAllZones true if all zones have to be considered as OFF
1802 private void schedulePowerOffJob(boolean switchOffAllZones) {
1803 logger.debug("Schedule power OFF job");
1804 cancelPowerOffJob();
1805 powerOffJob = scheduler.schedule(() -> {
1806 logger.debug("Power OFF job");
1808 if (switchOffAllZones) {
1809 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1810 handlePowerOffZone(zone);
1813 }, 2000, TimeUnit.MILLISECONDS);
1817 * Cancel the job that will consider the device as OFF
1819 private void cancelPowerOffJob() {
1820 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1821 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1822 powerOffJob.cancel(true);
1823 this.powerOffJob = null;
1828 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1830 private void schedulePowerOnJob() {
1831 logger.debug("Schedule power ON job");
1832 cancelPowerOnZoneJob(0);
1833 powerOnZoneJobs[0] = scheduler.schedule(() -> {
1834 synchronized (sequenceLock) {
1835 logger.debug("Power ON job");
1839 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1840 sendCommand(getVolumeDownCommand(0));
1842 sendCommand(getVolumeUpCommand(0));
1845 if (model.getNumberOfZones() > 1) {
1846 if (currentZone != 1
1847 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1848 selectZone(1, model.getZoneSelectCmd());
1849 } else if (!selectingRecord) {
1850 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1854 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1857 if (model.hasToneControl()) {
1858 if (model == RotelModel.RSX1065) {
1859 // No tone control select command
1860 sendCommand(RotelCommand.TREBLE_DOWN);
1862 sendCommand(RotelCommand.TREBLE_UP);
1864 sendCommand(RotelCommand.BASS_DOWN);
1866 sendCommand(RotelCommand.BASS_UP);
1869 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1874 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1875 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1876 sendCommand(RotelCommand.UPDATE_AUTO);
1877 Thread.sleep(SLEEP_INTV);
1879 if (model.hasSourceControl()) {
1880 sendCommand(RotelCommand.SOURCE);
1881 Thread.sleep(SLEEP_INTV);
1883 if (model.hasVolumeControl() || model.hasToneControl()) {
1884 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1885 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1886 sendCommand(RotelCommand.VOLUME_GET_MIN);
1887 Thread.sleep(SLEEP_INTV);
1888 sendCommand(RotelCommand.VOLUME_GET_MAX);
1889 Thread.sleep(SLEEP_INTV);
1891 if (model.hasToneControl()) {
1892 sendCommand(RotelCommand.TONE_MAX);
1893 Thread.sleep(SLEEP_INTV);
1895 // Wait enough to be sure to get the min/max values requested just before
1897 if (model.hasVolumeControl()) {
1898 sendCommand(RotelCommand.VOLUME_GET);
1899 Thread.sleep(SLEEP_INTV);
1900 if (model != RotelModel.RA11 && model != RotelModel.RA12
1901 && model != RotelModel.RCX1500) {
1902 sendCommand(RotelCommand.MUTE);
1903 Thread.sleep(SLEEP_INTV);
1906 if (model.hasToneControl()) {
1907 sendCommand(RotelCommand.BASS);
1908 Thread.sleep(SLEEP_INTV);
1909 sendCommand(RotelCommand.TREBLE);
1910 Thread.sleep(SLEEP_INTV);
1911 if (model.canGetBypassStatus()) {
1912 sendCommand(RotelCommand.TONE_CONTROLS);
1913 Thread.sleep(SLEEP_INTV);
1917 if (model.hasBalanceControl()) {
1918 sendCommand(RotelCommand.BALANCE);
1919 Thread.sleep(SLEEP_INTV);
1921 if (model.hasPlayControl()) {
1922 RotelSource source = sources[0];
1923 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1924 && (model != RotelModel.RCX1500 || source == null
1925 || !source.getName().equals("CD"))) {
1926 sendCommand(RotelCommand.PLAY_STATUS);
1927 Thread.sleep(SLEEP_INTV);
1929 sendCommand(RotelCommand.CD_PLAY_STATUS);
1930 Thread.sleep(SLEEP_INTV);
1933 if (model.hasDspControl()) {
1934 sendCommand(RotelCommand.DSP_MODE);
1935 Thread.sleep(SLEEP_INTV);
1937 if (model.canGetFrequency()) {
1938 sendCommand(RotelCommand.FREQUENCY);
1939 Thread.sleep(SLEEP_INTV);
1941 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1942 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1943 Thread.sleep(SLEEP_INTV);
1945 if (model.hasSpeakerGroups()) {
1946 sendCommand(RotelCommand.SPEAKER);
1947 Thread.sleep(SLEEP_INTV);
1949 if (model != RotelModel.RAP1580 && model != RotelModel.RSP1576
1950 && model != RotelModel.RSP1582) {
1951 sendCommand(RotelCommand.MODEL);
1952 Thread.sleep(SLEEP_INTV);
1953 sendCommand(RotelCommand.VERSION);
1954 Thread.sleep(SLEEP_INTV);
1958 sendCommand(RotelCommand.UPDATE_AUTO);
1959 Thread.sleep(SLEEP_INTV);
1960 if (model.hasSourceControl()) {
1961 if (model.getNumberOfZones() > 1) {
1962 sendCommand(RotelCommand.INPUT);
1964 sendCommand(RotelCommand.SOURCE);
1966 Thread.sleep(SLEEP_INTV);
1968 if (model.hasVolumeControl()) {
1969 sendCommand(RotelCommand.VOLUME_GET);
1970 Thread.sleep(SLEEP_INTV);
1971 sendCommand(RotelCommand.MUTE);
1972 Thread.sleep(SLEEP_INTV);
1974 if (model.hasToneControl()) {
1975 sendCommand(RotelCommand.BASS);
1976 Thread.sleep(SLEEP_INTV);
1977 sendCommand(RotelCommand.TREBLE);
1978 Thread.sleep(SLEEP_INTV);
1979 if (model.canGetBypassStatus()) {
1980 sendCommand(RotelCommand.TCBYPASS);
1981 Thread.sleep(SLEEP_INTV);
1984 if (model.hasBalanceControl()) {
1985 sendCommand(RotelCommand.BALANCE);
1986 Thread.sleep(SLEEP_INTV);
1988 if (model.hasPlayControl()) {
1989 sendCommand(RotelCommand.PLAY_STATUS);
1990 Thread.sleep(SLEEP_INTV);
1991 RotelSource source = sources[0];
1992 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1993 sendCommand(RotelCommand.TRACK);
1994 Thread.sleep(SLEEP_INTV);
1995 sendCommand(RotelCommand.RANDOM_MODE);
1996 Thread.sleep(SLEEP_INTV);
1997 sendCommand(RotelCommand.REPEAT_MODE);
1998 Thread.sleep(SLEEP_INTV);
2001 if (model.hasDspControl()) {
2002 sendCommand(RotelCommand.DSP_MODE);
2003 Thread.sleep(SLEEP_INTV);
2005 if (model.canGetFrequency()) {
2006 sendCommand(RotelCommand.FREQUENCY);
2007 Thread.sleep(SLEEP_INTV);
2009 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
2010 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
2011 Thread.sleep(SLEEP_INTV);
2013 if (model.hasSpeakerGroups()) {
2014 sendCommand(RotelCommand.SPEAKER);
2015 Thread.sleep(SLEEP_INTV);
2017 sendCommand(RotelCommand.MODEL);
2018 Thread.sleep(SLEEP_INTV);
2019 sendCommand(RotelCommand.VERSION);
2020 Thread.sleep(SLEEP_INTV);
2023 } catch (RotelException e) {
2024 logger.debug("Init sequence failed: {}", e.getMessage());
2025 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2026 "@text/offline.comm-error-init-sequence");
2028 } catch (InterruptedException e) {
2029 logger.debug("Init sequence interrupted: {}", e.getMessage());
2030 Thread.currentThread().interrupt();
2033 }, 2500, TimeUnit.MILLISECONDS);
2037 * Schedule the job to run with a few seconds delay when the zone power switched ON
2039 private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
2040 logger.debug("Schedule power ON zone {} job", numZone);
2041 cancelPowerOnZoneJob(numZone);
2042 powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
2043 synchronized (sequenceLock) {
2044 logger.debug("Power ON zone {} job", numZone);
2046 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
2047 selectZone(numZone, model.getZoneSelectCmd());
2048 sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
2050 sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
2053 } catch (RotelException e) {
2054 logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
2055 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2056 String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
2058 } catch (InterruptedException e) {
2059 logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
2060 Thread.currentThread().interrupt();
2063 }, 2500, TimeUnit.MILLISECONDS);
2067 * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
2069 private void cancelPowerOnZoneJob(int numZone) {
2070 ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
2071 if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
2072 powerOnZoneJob.cancel(true);
2073 powerOnZoneJobs[numZone] = null;
2078 * Schedule the reconnection job
2080 private void scheduleReconnectJob() {
2081 logger.debug("Schedule reconnect job");
2082 cancelReconnectJob();
2083 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2084 if (!connector.isConnected()) {
2085 logger.debug("Trying to reconnect...");
2088 String error = null;
2089 if (openConnection()) {
2090 synchronized (sequenceLock) {
2091 schedulePowerOffJob(true);
2093 sendCommand(model.getPowerStateCmd());
2094 } catch (RotelException e) {
2095 error = "@text/offline.comm-error-first-command-after-reconnection";
2096 logger.debug("First command after connection failed", e);
2097 cancelPowerOffJob();
2102 error = "@text/offline.comm-error-reconnection";
2104 if (error != null) {
2106 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
2107 handlePowerOffZone(zone);
2109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2111 updateStatus(ThingStatus.ONLINE);
2114 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2118 * Cancel the reconnection job
2120 private void cancelReconnectJob() {
2121 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2122 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2123 reconnectJob.cancel(true);
2124 this.reconnectJob = null;
2128 private void updateGroupChannelState(int numZone, String channel) {
2129 updateChannelState(String.format("zone%d#%s", numZone, channel));
2133 * Update the state of a channel
2135 * @param channel the channel
2137 private void updateChannelState(String channel) {
2138 if (!isLinked(channel)) {
2141 State state = UnDefType.UNDEF;
2142 RotelSource localSource;
2145 case CHANNEL_ZONE1_SOURCE:
2146 case CHANNEL_ZONE1_VOLUME:
2147 case CHANNEL_ZONE1_MUTE:
2148 case CHANNEL_ZONE1_BASS:
2149 case CHANNEL_ZONE1_TREBLE:
2150 case CHANNEL_ZONE1_BALANCE:
2151 case CHANNEL_ZONE1_FREQUENCY:
2154 case CHANNEL_ZONE2_POWER:
2155 case CHANNEL_ZONE2_SOURCE:
2156 case CHANNEL_ZONE2_VOLUME:
2157 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2158 case CHANNEL_ZONE2_MUTE:
2159 case CHANNEL_ZONE2_BASS:
2160 case CHANNEL_ZONE2_TREBLE:
2161 case CHANNEL_ZONE2_BALANCE:
2162 case CHANNEL_ZONE2_FREQUENCY:
2165 case CHANNEL_ZONE3_POWER:
2166 case CHANNEL_ZONE3_SOURCE:
2167 case CHANNEL_ZONE3_VOLUME:
2168 case CHANNEL_ZONE3_MUTE:
2169 case CHANNEL_ZONE3_BASS:
2170 case CHANNEL_ZONE3_TREBLE:
2171 case CHANNEL_ZONE3_BALANCE:
2172 case CHANNEL_ZONE3_FREQUENCY:
2175 case CHANNEL_ZONE4_POWER:
2176 case CHANNEL_ZONE4_SOURCE:
2177 case CHANNEL_ZONE4_VOLUME:
2178 case CHANNEL_ZONE4_MUTE:
2179 case CHANNEL_ZONE4_BASS:
2180 case CHANNEL_ZONE4_TREBLE:
2181 case CHANNEL_ZONE4_BALANCE:
2182 case CHANNEL_ZONE4_FREQUENCY:
2190 case CHANNEL_MAIN_POWER:
2191 case CHANNEL_ALL_POWER:
2192 case CHANNEL_ZONE2_POWER:
2193 case CHANNEL_ZONE3_POWER:
2194 case CHANNEL_ZONE4_POWER:
2195 Boolean powerZone = powers[numZone];
2196 if (powerZone != null) {
2197 state = OnOffType.from(powerZone.booleanValue());
2200 case CHANNEL_SOURCE:
2201 case CHANNEL_MAIN_SOURCE:
2202 case CHANNEL_ZONE1_SOURCE:
2203 case CHANNEL_ZONE2_SOURCE:
2204 case CHANNEL_ZONE3_SOURCE:
2205 case CHANNEL_ZONE4_SOURCE:
2206 localSource = sources[numZone];
2207 if (isPowerOn(numZone) && localSource != null) {
2208 state = new StringType(localSource.getName());
2211 case CHANNEL_MAIN_RECORD_SOURCE:
2212 localSource = recordSource;
2213 if (isPowerOn() && localSource != null) {
2214 state = new StringType(localSource.getName());
2218 case CHANNEL_MAIN_DSP:
2220 state = new StringType(dsp.getName());
2223 case CHANNEL_VOLUME:
2224 case CHANNEL_MAIN_VOLUME:
2225 case CHANNEL_ZONE1_VOLUME:
2226 case CHANNEL_ZONE2_VOLUME:
2227 case CHANNEL_ZONE3_VOLUME:
2228 case CHANNEL_ZONE4_VOLUME:
2229 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2230 long volumePct = Math
2231 .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2232 state = new PercentType(BigDecimal.valueOf(volumePct));
2235 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2236 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2237 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2238 state = new DecimalType(volumes[numZone]);
2242 case CHANNEL_MAIN_MUTE:
2243 case CHANNEL_ZONE1_MUTE:
2244 case CHANNEL_ZONE2_MUTE:
2245 case CHANNEL_ZONE3_MUTE:
2246 case CHANNEL_ZONE4_MUTE:
2247 if (isPowerOn(numZone)) {
2248 state = OnOffType.from(mutes[numZone]);
2252 case CHANNEL_MAIN_BASS:
2253 case CHANNEL_ZONE1_BASS:
2254 case CHANNEL_ZONE2_BASS:
2255 case CHANNEL_ZONE3_BASS:
2256 case CHANNEL_ZONE4_BASS:
2257 if (isPowerOn(numZone)) {
2258 state = new DecimalType(basses[numZone]);
2261 case CHANNEL_TREBLE:
2262 case CHANNEL_MAIN_TREBLE:
2263 case CHANNEL_ZONE1_TREBLE:
2264 case CHANNEL_ZONE2_TREBLE:
2265 case CHANNEL_ZONE3_TREBLE:
2266 case CHANNEL_ZONE4_TREBLE:
2267 if (isPowerOn(numZone)) {
2268 state = new DecimalType(trebles[numZone]);
2272 if (isPowerOn() && track > 0) {
2273 state = new DecimalType(track);
2276 case CHANNEL_RANDOM:
2278 state = OnOffType.from(randomMode);
2281 case CHANNEL_REPEAT:
2283 state = new StringType(repeatMode.name());
2286 case CHANNEL_PLAY_CONTROL:
2288 switch (playStatus) {
2290 state = PlayPauseType.PLAY;
2294 state = PlayPauseType.PAUSE;
2299 case CHANNEL_FREQUENCY:
2300 case CHANNEL_ZONE1_FREQUENCY:
2301 case CHANNEL_ZONE2_FREQUENCY:
2302 case CHANNEL_ZONE3_FREQUENCY:
2303 case CHANNEL_ZONE4_FREQUENCY:
2304 if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
2305 state = new DecimalType(frequencies[numZone]);
2309 state = new StringType(frontPanelLine1);
2312 state = new StringType(frontPanelLine2);
2314 case CHANNEL_BRIGHTNESS:
2315 case CHANNEL_ALL_BRIGHTNESS:
2316 if (isPowerOn() && model.hasDimmerControl()) {
2317 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2318 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2319 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2322 case CHANNEL_TCBYPASS:
2324 state = OnOffType.from(tcbypass);
2327 case CHANNEL_BALANCE:
2328 case CHANNEL_ZONE1_BALANCE:
2329 case CHANNEL_ZONE2_BALANCE:
2330 case CHANNEL_ZONE3_BALANCE:
2331 case CHANNEL_ZONE4_BALANCE:
2332 if (isPowerOn(numZone)) {
2333 state = new DecimalType(balances[numZone]);
2336 case CHANNEL_SPEAKER_A:
2338 state = OnOffType.from(speakera);
2341 case CHANNEL_SPEAKER_B:
2343 state = OnOffType.from(speakerb);
2349 updateState(channel, state);
2353 * Inform about the device / main zone power state
2355 * @return true if device / main zone power state is known and known as ON
2357 private boolean isPowerOn() {
2358 return isPowerOn(0);
2362 * Inform about the power state
2364 * @param numZone the zone number (1-4) or 0 for the device or main zone
2366 * @return true if power state is known and known as ON
2368 private boolean isPowerOn(int numZone) {
2369 if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
2370 throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
2372 Boolean power = powers[numZone];
2373 return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
2377 * Get the command to be used for POWER ON
2379 * @param numZone the zone number (2-4) or 0 for the device or main zone
2381 * @return the command
2383 private RotelCommand getPowerOnCommand(int numZone) {
2386 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2388 return RotelCommand.ZONE2_POWER_ON;
2390 return RotelCommand.ZONE3_POWER_ON;
2392 return RotelCommand.ZONE4_POWER_ON;
2394 throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
2399 * Get the command to be used for POWER OFF
2401 * @param numZone the zone number (2-4) or 0 for the device or main zone
2403 * @return the command
2405 private RotelCommand getPowerOffCommand(int numZone) {
2408 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2410 return RotelCommand.ZONE2_POWER_OFF;
2412 return RotelCommand.ZONE3_POWER_OFF;
2414 return RotelCommand.ZONE4_POWER_OFF;
2416 throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
2421 * Get the command to be used for VOLUME UP
2423 * @param numZone the zone number (1-4) or 0 for the device or main zone
2425 * @return the command
2427 private RotelCommand getVolumeUpCommand(int numZone) {
2430 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2432 return RotelCommand.ZONE1_VOLUME_UP;
2434 return RotelCommand.ZONE2_VOLUME_UP;
2436 return RotelCommand.ZONE3_VOLUME_UP;
2438 return RotelCommand.ZONE4_VOLUME_UP;
2440 throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
2445 * Get the command to be used for VOLUME DOWN
2447 * @param numZone the zone number (1-4) or 0 for the device or main zone
2449 * @return the command
2451 private RotelCommand getVolumeDownCommand(int numZone) {
2454 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2455 : RotelCommand.VOLUME_DOWN;
2457 return RotelCommand.ZONE1_VOLUME_DOWN;
2459 return RotelCommand.ZONE2_VOLUME_DOWN;
2461 return RotelCommand.ZONE3_VOLUME_DOWN;
2463 return RotelCommand.ZONE4_VOLUME_DOWN;
2465 throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
2470 * Get the command to be used for VOLUME SET
2472 * @param numZone the zone number (1-4) or 0 for the device
2474 * @return the command
2476 private RotelCommand getVolumeSetCommand(int numZone) {
2479 return RotelCommand.VOLUME_SET;
2481 return RotelCommand.ZONE1_VOLUME_SET;
2483 return RotelCommand.ZONE2_VOLUME_SET;
2485 return RotelCommand.ZONE3_VOLUME_SET;
2487 return RotelCommand.ZONE4_VOLUME_SET;
2489 throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
2494 * Get the command to be used for MUTE ON
2496 * @param numZone the zone number (1-4) or 0 for the device or main zone
2498 * @return the command
2500 private RotelCommand getMuteOnCommand(int numZone) {
2503 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2505 return RotelCommand.ZONE1_MUTE_ON;
2507 return RotelCommand.ZONE2_MUTE_ON;
2509 return RotelCommand.ZONE3_MUTE_ON;
2511 return RotelCommand.ZONE4_MUTE_ON;
2513 throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
2518 * Get the command to be used for MUTE OFF
2520 * @param numZone the zone number (1-4) or 0 for the device or main zone
2522 * @return the command
2524 private RotelCommand getMuteOffCommand(int numZone) {
2527 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2529 return RotelCommand.ZONE1_MUTE_OFF;
2531 return RotelCommand.ZONE2_MUTE_OFF;
2533 return RotelCommand.ZONE3_MUTE_OFF;
2535 return RotelCommand.ZONE4_MUTE_OFF;
2537 throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
2542 * Get the command to be used for MUTE TOGGLE
2544 * @param numZone the zone number (1-4) or 0 for the device or main zone
2546 * @return the command
2548 private RotelCommand getMuteToggleCommand(int numZone) {
2551 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2552 : RotelCommand.MUTE_TOGGLE;
2554 return RotelCommand.ZONE1_MUTE_TOGGLE;
2556 return RotelCommand.ZONE2_MUTE_TOGGLE;
2558 return RotelCommand.ZONE3_MUTE_TOGGLE;
2560 return RotelCommand.ZONE4_MUTE_TOGGLE;
2562 throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
2567 * Get the command to be used for BASS UP
2569 * @param numZone the zone number (1-4) or 0 for the device
2571 * @return the command
2573 private RotelCommand getBassUpCommand(int numZone) {
2576 return RotelCommand.BASS_UP;
2578 return RotelCommand.ZONE1_BASS_UP;
2580 return RotelCommand.ZONE2_BASS_UP;
2582 return RotelCommand.ZONE3_BASS_UP;
2584 return RotelCommand.ZONE4_BASS_UP;
2586 throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
2591 * Get the command to be used for BASS DOWN
2593 * @param numZone the zone number (1-4) or 0 for the device
2595 * @return the command
2597 private RotelCommand getBassDownCommand(int numZone) {
2600 return RotelCommand.BASS_DOWN;
2602 return RotelCommand.ZONE1_BASS_DOWN;
2604 return RotelCommand.ZONE2_BASS_DOWN;
2606 return RotelCommand.ZONE3_BASS_DOWN;
2608 return RotelCommand.ZONE4_BASS_DOWN;
2610 throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
2615 * Get the command to be used for BASS SET
2617 * @param numZone the zone number (1-4) or 0 for the device
2619 * @return the command
2621 private RotelCommand getBassSetCommand(int numZone) {
2624 return RotelCommand.BASS_SET;
2626 return RotelCommand.ZONE1_BASS_SET;
2628 return RotelCommand.ZONE2_BASS_SET;
2630 return RotelCommand.ZONE3_BASS_SET;
2632 return RotelCommand.ZONE4_BASS_SET;
2634 throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
2639 * Get the command to be used for TREBLE UP
2641 * @param numZone the zone number (1-4) or 0 for the device
2643 * @return the command
2645 private RotelCommand getTrebleUpCommand(int numZone) {
2648 return RotelCommand.TREBLE_UP;
2650 return RotelCommand.ZONE1_TREBLE_UP;
2652 return RotelCommand.ZONE2_TREBLE_UP;
2654 return RotelCommand.ZONE3_TREBLE_UP;
2656 return RotelCommand.ZONE4_TREBLE_UP;
2658 throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
2663 * Get the command to be used for TREBLE DOWN
2665 * @param numZone the zone number (1-4) or 0 for the device
2667 * @return the command
2669 private RotelCommand getTrebleDownCommand(int numZone) {
2672 return RotelCommand.TREBLE_DOWN;
2674 return RotelCommand.ZONE1_TREBLE_DOWN;
2676 return RotelCommand.ZONE2_TREBLE_DOWN;
2678 return RotelCommand.ZONE3_TREBLE_DOWN;
2680 return RotelCommand.ZONE4_TREBLE_DOWN;
2682 throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
2687 * Get the command to be used for TREBLE SET
2689 * @param numZone the zone number (1-4) or 0 for the device
2691 * @return the command
2693 private RotelCommand getTrebleSetCommand(int numZone) {
2696 return RotelCommand.TREBLE_SET;
2698 return RotelCommand.ZONE1_TREBLE_SET;
2700 return RotelCommand.ZONE2_TREBLE_SET;
2702 return RotelCommand.ZONE3_TREBLE_SET;
2704 return RotelCommand.ZONE4_TREBLE_SET;
2706 throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
2711 * Get the command to be used for BALANCE LEFT
2713 * @param numZone the zone number (1-4) or 0 for the device
2715 * @return the command
2717 private RotelCommand getBalanceLeftCommand(int numZone) {
2720 return RotelCommand.BALANCE_LEFT;
2722 return RotelCommand.ZONE1_BALANCE_LEFT;
2724 return RotelCommand.ZONE2_BALANCE_LEFT;
2726 return RotelCommand.ZONE3_BALANCE_LEFT;
2728 return RotelCommand.ZONE4_BALANCE_LEFT;
2730 throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
2735 * Get the command to be used for BALANCE RIGHT
2737 * @param numZone the zone number (1-4) or 0 for the device
2739 * @return the command
2741 private RotelCommand getBalanceRightCommand(int numZone) {
2744 return RotelCommand.BALANCE_RIGHT;
2746 return RotelCommand.ZONE1_BALANCE_RIGHT;
2748 return RotelCommand.ZONE2_BALANCE_RIGHT;
2750 return RotelCommand.ZONE3_BALANCE_RIGHT;
2752 return RotelCommand.ZONE4_BALANCE_RIGHT;
2754 throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
2759 * Get the command to be used for BALANCE SET
2761 * @param numZone the zone number (1-4) or 0 for the device
2763 * @return the command
2765 private RotelCommand getBalanceSetCommand(int numZone) {
2768 return RotelCommand.BALANCE_SET;
2770 return RotelCommand.ZONE1_BALANCE_SET;
2772 return RotelCommand.ZONE2_BALANCE_SET;
2774 return RotelCommand.ZONE3_BALANCE_SET;
2776 return RotelCommand.ZONE4_BALANCE_SET;
2778 throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
2782 private void sendCommand(RotelCommand cmd) throws RotelException {
2783 sendCommand(cmd, null);
2787 * Request the Rotel device to execute a command
2789 * @param cmd the command to execute
2790 * @param value the integer value to consider for volume, bass or treble adjustment
2792 * @throws RotelException - In case of any problem
2794 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2797 message = protocolHandler.buildCommandMessage(cmd, value);
2798 } catch (RotelException e) {
2799 // Command not supported
2800 logger.debug("sendCommand: {}", e.getMessage());
2803 connector.writeOutput(cmd, message);
2805 if (connector instanceof RotelSimuConnector) {
2806 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2807 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2808 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2809 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);