2 * Copyright (c) 2010-2023 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 int radioPreset;
122 private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
123 private String frontPanelLine1 = "";
124 private String frontPanelLine2 = "";
125 private int brightness;
126 private boolean tcbypass;
127 private int[] balances = { 0, 0, 0, 0, 0 };
128 private int minBalanceLevel;
129 private int maxBalanceLevel;
130 private boolean speakera;
131 private boolean speakerb;
133 private Object sequenceLock = new Object();
138 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
139 RotelCommandDescriptionOptionProvider commandDescriptionProvider, SerialPortManager serialPortManager) {
141 this.stateDescriptionProvider = stateDescriptionProvider;
142 this.commandDescriptionProvider = commandDescriptionProvider;
143 this.serialPortManager = serialPortManager;
144 this.model = DEFAULT_MODEL;
145 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
146 this.protocol = protocolHandler.getProtocol();
147 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
151 public void initialize() {
152 logger.debug("Start initializing handler for thing {}", getThing().getUID());
154 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
156 protocol = RotelProtocol.HEX;
157 if (config.protocol != null && !config.protocol.isEmpty()) {
159 protocol = RotelProtocol.getFromName(config.protocol);
160 } catch (RotelException e) {
161 // Invalid protocol name in configuration, HEX will be considered by default
164 Map<String, String> properties = editProperties();
165 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
166 if (property != null && !property.isEmpty()) {
168 protocol = RotelProtocol.getFromName(property);
169 } catch (RotelException e) {
170 // Invalid protocol name in thing property, HEX will be considered by default
174 logger.debug("rotelProtocol {}", protocol.getName());
176 switch (getThing().getThingTypeUID().getId()) {
177 case THING_TYPE_ID_RSP1066:
178 model = RotelModel.RSP1066;
180 case THING_TYPE_ID_RSP1068:
181 model = RotelModel.RSP1068;
183 case THING_TYPE_ID_RSP1069:
184 model = RotelModel.RSP1069;
186 case THING_TYPE_ID_RSP1098:
187 model = RotelModel.RSP1098;
189 case THING_TYPE_ID_RSP1570:
190 model = RotelModel.RSP1570;
192 case THING_TYPE_ID_RSP1572:
193 model = RotelModel.RSP1572;
195 case THING_TYPE_ID_RSX1055:
196 model = RotelModel.RSX1055;
198 case THING_TYPE_ID_RSX1056:
199 model = RotelModel.RSX1056;
201 case THING_TYPE_ID_RSX1057:
202 model = RotelModel.RSX1057;
204 case THING_TYPE_ID_RSX1058:
205 model = RotelModel.RSX1058;
207 case THING_TYPE_ID_RSX1065:
208 model = RotelModel.RSX1065;
210 case THING_TYPE_ID_RSX1067:
211 model = RotelModel.RSX1067;
213 case THING_TYPE_ID_RSX1550:
214 model = RotelModel.RSX1550;
216 case THING_TYPE_ID_RSX1560:
217 model = RotelModel.RSX1560;
219 case THING_TYPE_ID_RSX1562:
220 model = RotelModel.RSX1562;
222 case THING_TYPE_ID_A11:
223 model = RotelModel.A11;
225 case THING_TYPE_ID_A12:
226 model = RotelModel.A12;
228 case THING_TYPE_ID_A14:
229 model = RotelModel.A14;
231 case THING_TYPE_ID_CD11:
232 model = RotelModel.CD11;
234 case THING_TYPE_ID_CD14:
235 model = RotelModel.CD14;
237 case THING_TYPE_ID_RA11:
238 model = RotelModel.RA11;
240 case THING_TYPE_ID_RA12:
241 model = RotelModel.RA12;
243 case THING_TYPE_ID_RA1570:
244 model = RotelModel.RA1570;
246 case THING_TYPE_ID_RA1572:
247 model = RotelModel.RA1572;
249 case THING_TYPE_ID_RA1592:
250 if (protocol == RotelProtocol.ASCII_V1) {
251 model = RotelModel.RA1592_V1;
253 model = RotelModel.RA1592_V2;
256 case THING_TYPE_ID_RAP1580:
257 model = RotelModel.RAP1580;
259 case THING_TYPE_ID_RC1570:
260 model = RotelModel.RC1570;
262 case THING_TYPE_ID_RC1572:
263 model = RotelModel.RC1572;
265 case THING_TYPE_ID_RC1590:
266 if (protocol == RotelProtocol.ASCII_V1) {
267 model = RotelModel.RC1590_V1;
269 model = RotelModel.RC1590_V2;
272 case THING_TYPE_ID_RCD1570:
273 model = RotelModel.RCD1570;
275 case THING_TYPE_ID_RCD1572:
276 model = RotelModel.RCD1572;
278 case THING_TYPE_ID_RCX1500:
279 model = RotelModel.RCX1500;
281 case THING_TYPE_ID_RDD1580:
282 model = RotelModel.RDD1580;
284 case THING_TYPE_ID_RDG1520:
285 case THING_TYPE_ID_RT09:
286 model = RotelModel.RDG1520;
288 case THING_TYPE_ID_RSP1576:
289 model = RotelModel.RSP1576;
291 case THING_TYPE_ID_RSP1582:
292 model = RotelModel.RSP1582;
294 case THING_TYPE_ID_RT11:
295 model = RotelModel.RT11;
297 case THING_TYPE_ID_RT1570:
298 model = RotelModel.RT1570;
300 case THING_TYPE_ID_T11:
301 model = RotelModel.T11;
303 case THING_TYPE_ID_T14:
304 model = RotelModel.T14;
306 case THING_TYPE_ID_C8:
307 model = RotelModel.C8;
309 case THING_TYPE_ID_M8:
310 model = RotelModel.M8;
312 case THING_TYPE_ID_P5:
313 model = RotelModel.P5;
315 case THING_TYPE_ID_S5:
316 model = RotelModel.S5;
318 case THING_TYPE_ID_X3:
319 model = RotelModel.X3;
321 case THING_TYPE_ID_X5:
322 model = RotelModel.X5;
325 model = DEFAULT_MODEL;
329 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
330 Map<RotelSource, String> sourcesLabels = new HashMap<>();
332 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
334 if (model.hasVolumeControl()) {
335 maxVolume = model.getVolumeMax();
336 if (!model.hasDirectVolumeControl()) {
338 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
339 minVolume, maxVolume);
342 if (model.hasToneControl()) {
343 maxToneLevel = model.getToneLevelMax();
344 minToneLevel = -maxToneLevel;
346 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
347 minToneLevel, maxToneLevel);
349 if (model.hasBalanceControl()) {
350 maxBalanceLevel = model.getBalanceLevelMax();
351 minBalanceLevel = -maxBalanceLevel;
352 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
353 minBalanceLevel, maxBalanceLevel);
356 powerControlPerZone = model.hasPowerControlPerZone();
358 // Check configuration settings
359 String configError = null;
360 if ((config.serialPort == null || config.serialPort.isEmpty())
361 && (config.host == null || config.host.isEmpty())) {
362 configError = "@text/offline.config-error-unknown-serialport-and-host";
363 } else if (config.host == null || config.host.isEmpty()) {
364 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
365 configError = "@text/offline.config-error-invalid-serial-over-ip";
368 if (config.port == null) {
369 configError = "@text/offline.config-error-unknown-port";
370 } else if (config.port <= 0) {
371 configError = "@text/offline.config-error-invalid-port";
375 if (configError != null) {
376 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
378 for (RotelSource src : model.getSources()) {
379 // Consider custom input labels
381 switch (src.getName()) {
383 label = config.inputLabelCd;
386 label = config.inputLabelTuner;
389 label = config.inputLabelTape;
392 label = config.inputLabelPhono;
395 label = config.inputLabelVideo1;
398 label = config.inputLabelVideo2;
401 label = config.inputLabelVideo3;
404 label = config.inputLabelVideo4;
407 label = config.inputLabelVideo5;
410 label = config.inputLabelVideo6;
413 label = config.inputLabelUsb;
416 label = config.inputLabelMulti;
421 if (label != null && !label.isEmpty()) {
422 sourcesCustomLabels.put(src, label);
424 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
427 if (protocol == RotelProtocol.HEX) {
428 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
429 } else if (protocol == RotelProtocol.ASCII_V1) {
430 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
432 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
435 if (USE_SIMULATED_DEVICE) {
436 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
437 } else if (config.serialPort != null) {
438 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
439 protocolHandler, readerThreadName);
441 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
444 if (model.hasSourceControl()) {
445 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
446 getStateOptions(model.getSources(), sourcesCustomLabels));
447 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
448 getStateOptions(model.getSources(), sourcesCustomLabels));
449 stateDescriptionProvider.setStateOptions(
450 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
451 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
453 if (model.hasZoneSourceControl(1)) {
454 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
455 getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
457 if (model.hasZoneSourceControl(2)) {
458 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
459 getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
461 if (model.hasZoneSourceControl(3)) {
462 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
463 getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
465 if (model.hasZoneSourceControl(4)) {
466 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
467 getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
469 if (model.hasDspControl()) {
470 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
471 model.getDspStateOptions());
472 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
473 model.getDspStateOptions());
476 List<CommandOption> options = model.getOtherCommandsOptions(protocol);
477 if (!options.isEmpty()) {
478 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_OTHER_COMMAND),
480 commandDescriptionProvider
481 .setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_OTHER_COMMAND), options);
484 updateStatus(ThingStatus.UNKNOWN);
486 scheduleReconnectJob();
489 logger.debug("Finished initializing!");
493 public void dispose() {
494 logger.debug("Disposing handler for thing {}", getThing().getUID());
496 for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
497 cancelPowerOnZoneJob(zone);
499 cancelReconnectJob();
504 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
505 List<StateOption> options = new ArrayList<>();
506 for (RotelSource item : list) {
507 String label = sourcesLabels.get(item);
508 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
514 public void handleCommand(ChannelUID channelUID, Command command) {
515 String channel = channelUID.getId();
517 if (getThing().getStatus() != ThingStatus.ONLINE) {
518 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
522 if (command instanceof RefreshType) {
523 updateChannelState(channel);
527 if (!connector.isConnected()) {
528 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
534 case CHANNEL_ZONE1_SOURCE:
535 case CHANNEL_ZONE1_VOLUME:
536 case CHANNEL_ZONE1_MUTE:
537 case CHANNEL_ZONE1_BASS:
538 case CHANNEL_ZONE1_TREBLE:
539 case CHANNEL_ZONE1_BALANCE:
542 case CHANNEL_ZONE2_POWER:
543 case CHANNEL_ZONE2_SOURCE:
544 case CHANNEL_ZONE2_VOLUME:
545 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
546 case CHANNEL_ZONE2_MUTE:
547 case CHANNEL_ZONE2_BASS:
548 case CHANNEL_ZONE2_TREBLE:
549 case CHANNEL_ZONE2_BALANCE:
552 case CHANNEL_ZONE3_POWER:
553 case CHANNEL_ZONE3_SOURCE:
554 case CHANNEL_ZONE3_VOLUME:
555 case CHANNEL_ZONE3_MUTE:
556 case CHANNEL_ZONE3_BASS:
557 case CHANNEL_ZONE3_TREBLE:
558 case CHANNEL_ZONE3_BALANCE:
561 case CHANNEL_ZONE4_POWER:
562 case CHANNEL_ZONE4_SOURCE:
563 case CHANNEL_ZONE4_VOLUME:
564 case CHANNEL_ZONE4_MUTE:
565 case CHANNEL_ZONE4_BASS:
566 case CHANNEL_ZONE4_TREBLE:
567 case CHANNEL_ZONE4_BALANCE:
576 boolean success = true;
577 synchronized (sequenceLock) {
581 case CHANNEL_MAIN_POWER:
582 case CHANNEL_ZONE2_POWER:
583 case CHANNEL_ZONE3_POWER:
584 case CHANNEL_ZONE4_POWER:
585 if (numZone == 0 || model.hasZoneCommands(numZone)) {
586 handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
587 } else if (numZone == 2 && model.getNumberOfZones() == 2) {
588 if (isPowerOn() || isPowerOn(numZone)) {
589 selectZone(2, model.getZoneSelectCmd());
591 sendCommand(RotelCommand.ZONE_SELECT);
594 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
597 case CHANNEL_ALL_POWER:
598 handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
601 case CHANNEL_MAIN_SOURCE:
602 case CHANNEL_ZONE1_SOURCE:
603 case CHANNEL_ZONE2_SOURCE:
604 case CHANNEL_ZONE3_SOURCE:
605 case CHANNEL_ZONE4_SOURCE:
606 if (!isPowerOn(numZone)) {
608 logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
609 numZone == 0 ? "device" : "zone " + numZone);
610 } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
611 src = model.getSourceFromName(command.toString());
613 cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
615 cmd = src.getZoneCommand(numZone);
619 if (model.canGetFrequency()) {
620 // send <new-source> returns
621 // 1.) the selected <new-source>
622 // 2.) the used frequency
624 // at response-time the frequency has the value of <old-source>
625 // so we must wait a short moment to get the frequency of <new-source>
627 sendCommand(RotelCommand.FREQUENCY);
629 updateChannelState(CHANNEL_FREQUENCY);
633 logger.debug("Command {} from channel {} failed: undefined source command", command,
636 } else if (numZone == 2 && model.getNumberOfZones() > 1) {
637 src = model.getSourceFromName(command.toString());
638 cmd = src.getCommand();
640 selectZone(2, model.getZoneSelectCmd());
642 if (model.canGetFrequency()) {
643 // send <new-source> returns
644 // 1.) the selected <new-source>
645 // 2.) the used frequency
647 // at response-time the frequency has the value of <old-source>
648 // so we must wait a short moment to get the frequency of <new-source>
650 sendCommand(RotelCommand.FREQUENCY);
652 updateChannelState(CHANNEL_FREQUENCY);
656 logger.debug("Command {} from channel {} failed: undefined source command", command,
661 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
664 case CHANNEL_MAIN_RECORD_SOURCE:
667 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
668 } else if (model.hasOtherThanPrimaryCommands()) {
669 src = model.getSourceFromName(command.toString());
670 cmd = src.getRecordCommand();
675 logger.debug("Command {} from channel {} failed: undefined record source command",
679 src = model.getSourceFromName(command.toString());
680 cmd = src.getCommand();
682 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
687 logger.debug("Command {} from channel {} failed: undefined source command", command,
693 case CHANNEL_MAIN_DSP:
696 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
698 sendCommand(model.getCommandFromDspName(command.toString()));
702 case CHANNEL_MAIN_VOLUME:
703 case CHANNEL_MAIN_VOLUME_UP_DOWN:
704 case CHANNEL_ZONE1_VOLUME:
705 case CHANNEL_ZONE2_VOLUME:
706 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
707 case CHANNEL_ZONE3_VOLUME:
708 case CHANNEL_ZONE4_VOLUME:
709 if (!isPowerOn(numZone)) {
711 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
712 numZone == 0 ? "device" : "zone " + numZone);
713 } else if (fixedVolumeZones[numZone]) {
715 logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
716 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
717 handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
718 getVolumeDownCommand(numZone),
719 CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
720 || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
721 : getVolumeSetCommand(numZone));
722 } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
723 selectZone(2, model.getZoneSelectCmd());
724 handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
725 RotelCommand.VOLUME_DOWN,
726 CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
729 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
733 case CHANNEL_MAIN_MUTE:
734 case CHANNEL_ZONE1_MUTE:
735 case CHANNEL_ZONE2_MUTE:
736 case CHANNEL_ZONE3_MUTE:
737 case CHANNEL_ZONE4_MUTE:
738 if (!isPowerOn(numZone)) {
740 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
741 numZone == 0 ? "device" : "zone " + numZone);
742 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
743 handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
744 getMuteOnCommand(numZone), getMuteOffCommand(numZone),
745 getMuteToggleCommand(numZone));
748 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
752 case CHANNEL_MAIN_BASS:
753 case CHANNEL_ZONE1_BASS:
754 case CHANNEL_ZONE2_BASS:
755 case CHANNEL_ZONE3_BASS:
756 case CHANNEL_ZONE4_BASS:
757 if (!isPowerOn(numZone)) {
759 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
760 numZone == 0 ? "device" : "zone " + numZone);
761 } else if (tcbypass) {
763 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
765 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
766 handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
767 getBassDownCommand(numZone), getBassSetCommand(numZone));
770 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
774 case CHANNEL_MAIN_TREBLE:
775 case CHANNEL_ZONE1_TREBLE:
776 case CHANNEL_ZONE2_TREBLE:
777 case CHANNEL_ZONE3_TREBLE:
778 case CHANNEL_ZONE4_TREBLE:
779 if (!isPowerOn(numZone)) {
781 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
782 numZone == 0 ? "device" : "zone " + numZone);
783 } else if (tcbypass) {
785 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
787 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
788 handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
789 getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
792 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
795 case CHANNEL_PLAY_CONTROL:
798 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
799 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
800 sendCommand(RotelCommand.PLAY);
801 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
802 sendCommand(RotelCommand.PAUSE);
803 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
804 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
805 Thread.sleep(SLEEP_INTV);
806 sendCommand(RotelCommand.PLAY_STATUS);
808 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
809 sendCommand(RotelCommand.TRACK_FWD);
810 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
811 sendCommand(RotelCommand.TRACK_BACK);
814 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
820 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
821 } else if (command instanceof OnOffType) {
822 sendCommand(RotelCommand.RANDOM_TOGGLE);
825 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
831 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
833 RotelRepeatMode currentMode = repeatMode;
834 RotelRepeatMode mode = RotelRepeatMode.OFF;
836 mode = RotelRepeatMode.getFromName(command.toString());
837 if (mode == currentMode) {
839 logger.debug("Command {} from channel {} ignored: no change requested", command,
842 } catch (RotelException e) {
844 logger.debug("Command {} from channel {} failed: invalid command value", command,
848 // Toggle TRACK -> DISC -> OFF
849 sendCommand(RotelCommand.REPEAT_TOGGLE);
850 if ((mode == RotelRepeatMode.OFF && currentMode == RotelRepeatMode.TRACK)
851 || (mode == RotelRepeatMode.TRACK && currentMode == RotelRepeatMode.DISC)
852 || (mode == RotelRepeatMode.DISC && currentMode == RotelRepeatMode.OFF)) {
853 Thread.sleep(SLEEP_INTV);
854 sendCommand(RotelCommand.REPEAT_TOGGLE);
859 case CHANNEL_RADIO_PRESET:
862 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
865 if (radioPreset > 0 && command instanceof IncreaseDecreaseType
866 && command == IncreaseDecreaseType.INCREASE) {
867 value = radioPreset + 1;
868 } else if (radioPreset > 0 && command instanceof IncreaseDecreaseType
869 && command == IncreaseDecreaseType.DECREASE) {
870 value = radioPreset - 1;
871 } else if (command instanceof DecimalType decimalCommand) {
872 value = decimalCommand.intValue();
874 if (value >= 1 && value <= 30) {
875 RotelSource source = sources[0];
876 RotelCommand presetCallCmd = source == null ? null : getRadioPresetCallCommand(source);
877 if (presetCallCmd != null) {
878 sendCommand(presetCallCmd, value);
879 // In ASCII V2, the previous command will return nothing
880 RotelCommand presetGetCmd = source == null ? null
881 : getRadioPresetGetCommand(source);
882 if (protocol == RotelProtocol.ASCII_V2 && presetGetCmd != null) {
883 Thread.sleep(SLEEP_INTV);
884 sendCommand(presetGetCmd);
888 logger.debug("Command {} from channel {} ignored: current source is not radio",
893 logger.debug("Command {} from channel {} ignored: value out of bounds", command,
898 case CHANNEL_BRIGHTNESS:
899 case CHANNEL_ALL_BRIGHTNESS:
902 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
903 } else if (!model.hasDimmerControl()) {
905 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
906 } else if (command instanceof PercentType percentCommand) {
907 int dimmer = (int) Math.round(percentCommand.doubleValue() / 100.0
908 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
909 + model.getDimmerLevelMin();
910 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
913 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
916 case CHANNEL_TCBYPASS:
919 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
920 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
922 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
924 handleTcbypassCmd(channel, command,
925 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
926 : RotelCommand.TCBYPASS_ON,
927 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
928 : RotelCommand.TCBYPASS_OFF);
931 case CHANNEL_BALANCE:
932 case CHANNEL_ZONE1_BALANCE:
933 case CHANNEL_ZONE2_BALANCE:
934 case CHANNEL_ZONE3_BALANCE:
935 case CHANNEL_ZONE4_BALANCE:
936 if (!isPowerOn(numZone)) {
938 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
939 numZone == 0 ? "device" : "zone " + numZone);
940 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
942 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
944 handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
945 getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
948 case CHANNEL_SPEAKER_A:
951 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
953 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
954 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
957 case CHANNEL_SPEAKER_B:
960 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
962 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
963 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
966 case CHANNEL_OTHER_COMMAND:
967 case CHANNEL_MAIN_OTHER_COMMAND:
970 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
973 cmd = RotelCommand.getFromName(command.toString());
974 } catch (RotelException e) {
976 logger.debug("Command {} from channel {} failed: undefined command", command, channel);
986 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
990 logger.debug("Command {} from channel {} succeeded", command, channel);
992 updateChannelState(channel);
994 } catch (RotelException e) {
995 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
996 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
997 "@text/offline.comm-error-sending-command");
999 scheduleReconnectJob();
1000 } catch (InterruptedException e) {
1001 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
1002 Thread.currentThread().interrupt();
1008 * Handle a power ON/OFF command
1010 * @param channel the channel
1011 * @param command the received channel command (OnOffType)
1012 * @param onCmd the command to be sent to the device to power it ON
1013 * @param offCmd the command to be sent to the device to power it OFF
1015 * @throws RotelException in case of communication error with the device
1017 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1018 throws RotelException {
1019 if (command instanceof OnOffType && command == OnOffType.ON) {
1021 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
1022 sendCommand(offCmd);
1024 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1029 * Handle a volume command
1031 * @param current the current volume
1032 * @param channel the channel
1033 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1034 * @param upCmd the command to be sent to the device to increase the volume
1035 * @param downCmd the command to be sent to the device to decrease the volume
1036 * @param setCmd the command to be sent to the device to set the volume at a value
1038 * @throws RotelException in case of communication error with the device
1040 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
1041 @Nullable RotelCommand setCmd) throws RotelException {
1042 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1044 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1045 sendCommand(downCmd);
1046 } else if (command instanceof DecimalType decimalCommand && setCmd == null) {
1047 int value = decimalCommand.intValue();
1048 if (value >= minVolume && value <= maxVolume) {
1049 if (value > current) {
1051 } else if (value < current) {
1052 sendCommand(downCmd);
1055 } else if (command instanceof PercentType percentCommand && setCmd != null) {
1056 int value = (int) Math.round(percentCommand.doubleValue() / 100.0 * (maxVolume - minVolume)) + minVolume;
1057 sendCommand(setCmd, value);
1059 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1064 * Handle a mute command
1066 * @param onlyToggle true if only the toggle command must be used
1067 * @param channel the channel
1068 * @param command the received channel command (OnOffType)
1069 * @param onCmd the command to be sent to the device to mute
1070 * @param offCmd the command to be sent to the device to unmute
1071 * @param toggleCmd the command to be sent to the device to toggle the mute state
1073 * @throws RotelException in case of communication error with the device
1075 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1076 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1077 if (command instanceof OnOffType) {
1079 sendCommand(toggleCmd);
1080 } else if (command == OnOffType.ON) {
1082 } else if (command == OnOffType.OFF) {
1083 sendCommand(offCmd);
1086 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1091 * Handle a tone level adjustment command (bass or treble)
1093 * @param current the current tone level
1094 * @param channel the channel
1095 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1096 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1097 * @param upCmd the command to be sent to the device to increase the tone level
1098 * @param downCmd the command to be sent to the device to decrease the tone level
1099 * @param setCmd the command to be sent to the device to set the tone level at a value
1101 * @throws RotelException in case of communication error with the device
1102 * @throws InterruptedException in case of interruption during a thread sleep
1104 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1105 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1106 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1107 selectToneControl(nbSelect);
1109 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1110 selectToneControl(nbSelect);
1111 sendCommand(downCmd);
1112 } else if (command instanceof DecimalType decimalCommand) {
1113 int value = decimalCommand.intValue();
1114 if (value >= minToneLevel && value <= maxToneLevel) {
1115 if (protocol != RotelProtocol.HEX) {
1116 sendCommand(setCmd, value);
1117 } else if (value > current) {
1118 selectToneControl(nbSelect);
1120 } else if (value < current) {
1121 selectToneControl(nbSelect);
1122 sendCommand(downCmd);
1126 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1131 * Handle a tcbypass command (only for ASCII protocol)
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 bypass_on
1136 * @param offCmd the command to be sent to the device to bypass_off
1138 * @throws RotelException in case of communication error with the device
1140 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1141 throws RotelException, InterruptedException {
1142 if (command instanceof OnOffType) {
1143 if (command == OnOffType.ON) {
1147 updateChannelState(CHANNEL_BASS);
1148 updateChannelState(CHANNEL_TREBLE);
1149 } else if (command == OnOffType.OFF) {
1150 sendCommand(offCmd);
1152 sendCommand(RotelCommand.BASS);
1154 sendCommand(RotelCommand.TREBLE);
1157 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1162 * Handle a speaker command
1164 * @param onlyToggle true if only the toggle command must be used
1165 * @param channel the channel
1166 * @param command the received channel command (OnOffType)
1167 * @param onCmd the command to be sent to the device to speaker_x_on
1168 * @param offCmd the command to be sent to the device to speaker_x_off
1169 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1171 * @throws RotelException in case of communication error with the device
1173 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1174 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1175 if (command instanceof OnOffType) {
1177 sendCommand(toggleCmd);
1178 } else if (command == OnOffType.ON) {
1180 } else if (command == OnOffType.OFF) {
1181 sendCommand(offCmd);
1184 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1189 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1191 * @param channel the channel
1192 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1193 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1194 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1195 * @param setCmd the command to be sent to the device to set the balance at a value
1197 * @throws RotelException in case of communication error with the device
1198 * @throws InterruptedException in case of interruption during a thread sleep
1200 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1201 RotelCommand setCmd) throws RotelException, InterruptedException {
1202 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1203 sendCommand(rightCmd);
1204 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1205 sendCommand(leftCmd);
1206 } else if (command instanceof DecimalType decimalCommand) {
1207 int value = decimalCommand.intValue();
1208 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1209 sendCommand(setCmd, value);
1212 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1217 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1219 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1221 * @throws RotelException in case of communication error with the device
1222 * @throws InterruptedException in case of interruption during a thread sleep
1224 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1225 // No tone control select command for RSX-1065
1226 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1227 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1232 * Run a sequence of commands to display a particular zone on the device front panel
1234 * @param zone the zone to be displayed (1 for main zone)
1235 * @param selectCommand the command to be sent to the device to switch the display between zones
1237 * @throws RotelException in case of communication error with the device
1238 * @throws InterruptedException in case of interruption during a thread sleep
1240 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1241 throws RotelException, InterruptedException {
1242 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
1243 && selectCommand != null) {
1245 if (zone < currentZone) {
1246 nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
1247 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1251 nbSelect = zone - currentZone;
1252 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1253 && !selectingRecord) {
1257 selectFeature(nbSelect, null, selectCommand);
1262 * Run a sequence of commands to display a particular feature on the device front panel
1264 * @param nbSelect the number of select commands to be run
1265 * @param preCmd the initial command to be sent to the device (before the select commands)
1266 * @param selectCmd the select command to be sent to the device
1268 * @throws RotelException in case of communication error with the device
1269 * @throws InterruptedException in case of interruption during a thread sleep
1271 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1272 throws RotelException, InterruptedException {
1273 if (protocol == RotelProtocol.HEX) {
1274 if (preCmd != null) {
1275 sendCommand(preCmd);
1278 for (int i = 1; i <= nbSelect; i++) {
1279 sendCommand(selectCmd);
1286 * Open the connection with the Rotel device
1288 * @return true if the connection is opened successfully or flase if not
1290 private synchronized boolean openConnection() {
1291 protocolHandler.addEventListener(this);
1294 } catch (RotelException e) {
1295 logger.debug("openConnection() failed", e);
1297 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1298 return connector.isConnected();
1302 * Close the connection with the Rotel device
1304 private synchronized void closeConnection() {
1306 protocolHandler.removeEventListener(this);
1307 logger.debug("closeConnection(): disconnected");
1311 public void onNewMessageEvent(EventObject event) {
1312 cancelPowerOffJob();
1314 RotelMessageEvent evt = (RotelMessageEvent) event;
1315 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1317 String key = evt.getKey();
1318 String value = evt.getValue().trim();
1319 if (!KEY_ERROR.equals(key)) {
1320 updateStatus(ThingStatus.ONLINE);
1324 case KEY_INPUT_ZONE1:
1325 case KEY_VOLUME_ZONE1:
1326 case KEY_MUTE_ZONE1:
1327 case KEY_BASS_ZONE1:
1328 case KEY_TREBLE_ZONE1:
1329 case KEY_BALANCE_ZONE1:
1330 case KEY_FREQ_ZONE1:
1333 case KEY_POWER_ZONE2:
1334 case KEY_SOURCE_ZONE2:
1335 case KEY_INPUT_ZONE2:
1336 case KEY_VOLUME_ZONE2:
1337 case KEY_MUTE_ZONE2:
1338 case KEY_BASS_ZONE2:
1339 case KEY_TREBLE_ZONE2:
1340 case KEY_BALANCE_ZONE2:
1341 case KEY_FREQ_ZONE2:
1344 case KEY_POWER_ZONE3:
1345 case KEY_SOURCE_ZONE3:
1346 case KEY_INPUT_ZONE3:
1347 case KEY_VOLUME_ZONE3:
1348 case KEY_MUTE_ZONE3:
1349 case KEY_BASS_ZONE3:
1350 case KEY_TREBLE_ZONE3:
1351 case KEY_BALANCE_ZONE3:
1352 case KEY_FREQ_ZONE3:
1355 case KEY_POWER_ZONE4:
1356 case KEY_SOURCE_ZONE4:
1357 case KEY_INPUT_ZONE4:
1358 case KEY_VOLUME_ZONE4:
1359 case KEY_MUTE_ZONE4:
1360 case KEY_BASS_ZONE4:
1361 case KEY_TREBLE_ZONE4:
1362 case KEY_BALANCE_ZONE4:
1363 case KEY_FREQ_ZONE4:
1370 if (key.startsWith(KEY_FM_PRESET)) {
1372 preset = Integer.parseInt(key.substring(KEY_FM_PRESET.length()));
1373 } catch (NumberFormatException e) {
1374 // Considering the Rotel protocol, the parsing could not fail in practice.
1375 // In case it would fail, 0 will be considered as preset, meaning undefined.
1377 key = KEY_FM_PRESET;
1378 } else if (key.startsWith(KEY_DAB_PRESET)) {
1380 preset = Integer.parseInt(key.substring(KEY_DAB_PRESET.length()));
1381 } catch (NumberFormatException e) {
1382 // Considering the Rotel protocol, the parsing could not fail in practice.
1383 // In case it would fail, 0 will be considered as preset, meaning undefined.
1385 key = KEY_DAB_PRESET;
1386 } else if (key.startsWith(KEY_IRADIO_PRESET)) {
1388 preset = Integer.parseInt(key.substring(KEY_IRADIO_PRESET.length()));
1389 } catch (NumberFormatException e) {
1390 // Considering the Rotel protocol, the parsing could not fail in practice.
1391 // In case it would fail, 0 will be considered as preset, meaning undefined.
1393 key = KEY_IRADIO_PRESET;
1399 logger.debug("Reading feedback message failed");
1400 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1401 "@text/offline.comm-error-reading-thread");
1405 frontPanelLine1 = value;
1406 updateChannelState(CHANNEL_LINE1);
1409 frontPanelLine2 = value;
1410 updateChannelState(CHANNEL_LINE2);
1413 currentZone = Integer.parseInt(value);
1415 case KEY_RECORD_SEL:
1416 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1419 if (POWER_ON.equalsIgnoreCase(value)) {
1421 } else if (STANDBY.equalsIgnoreCase(value)) {
1423 if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
1424 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1425 handlePowerOffZone(zone);
1428 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1429 schedulePowerOffJob(false);
1431 throw new RotelException("Invalid value");
1434 case KEY_POWER_ZONE2:
1435 case KEY_POWER_ZONE3:
1436 case KEY_POWER_ZONE4:
1437 if (POWER_ON.equalsIgnoreCase(value)) {
1438 handlePowerOnZone(numZone);
1439 } else if (STANDBY.equalsIgnoreCase(value)) {
1440 handlePowerOffZone(numZone);
1442 throw new RotelException("Invalid value");
1445 case KEY_POWER_MODE:
1446 logger.debug("Power mode is set to {}", value);
1448 case KEY_VOLUME_MIN:
1449 minVolume = Integer.parseInt(value);
1450 if (!model.hasDirectVolumeControl()) {
1451 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1455 case KEY_VOLUME_MAX:
1456 maxVolume = Integer.parseInt(value);
1457 if (!model.hasDirectVolumeControl()) {
1458 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1463 case KEY_VOLUME_ZONE1:
1464 case KEY_VOLUME_ZONE2:
1465 case KEY_VOLUME_ZONE3:
1466 case KEY_VOLUME_ZONE4:
1467 fixedVolumeZones[numZone] = false;
1468 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1469 fixedVolumeZones[numZone] = true;
1470 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1471 volumes[numZone] = minVolume;
1472 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1473 volumes[numZone] = maxVolume;
1475 volumes[numZone] = Integer.parseInt(value);
1478 updateChannelState(CHANNEL_VOLUME);
1479 updateChannelState(CHANNEL_MAIN_VOLUME);
1480 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1482 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1483 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1487 case KEY_MUTE_ZONE1:
1488 case KEY_MUTE_ZONE2:
1489 case KEY_MUTE_ZONE3:
1490 case KEY_MUTE_ZONE4:
1491 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1492 mutes[numZone] = true;
1494 updateChannelState(CHANNEL_MUTE);
1495 updateChannelState(CHANNEL_MAIN_MUTE);
1497 updateGroupChannelState(numZone, CHANNEL_MUTE);
1499 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1500 mutes[numZone] = false;
1502 updateChannelState(CHANNEL_MUTE);
1503 updateChannelState(CHANNEL_MAIN_MUTE);
1505 updateGroupChannelState(numZone, CHANNEL_MUTE);
1508 throw new RotelException("Invalid value");
1512 maxToneLevel = Integer.parseInt(value);
1513 minToneLevel = -maxToneLevel;
1515 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1516 minToneLevel, maxToneLevel);
1519 case KEY_BASS_ZONE1:
1520 case KEY_BASS_ZONE2:
1521 case KEY_BASS_ZONE3:
1522 case KEY_BASS_ZONE4:
1523 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1524 basses[numZone] = minToneLevel;
1525 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1526 basses[numZone] = maxToneLevel;
1528 basses[numZone] = Integer.parseInt(value);
1531 updateChannelState(CHANNEL_BASS);
1532 updateChannelState(CHANNEL_MAIN_BASS);
1534 updateGroupChannelState(numZone, CHANNEL_BASS);
1538 case KEY_TREBLE_ZONE1:
1539 case KEY_TREBLE_ZONE2:
1540 case KEY_TREBLE_ZONE3:
1541 case KEY_TREBLE_ZONE4:
1542 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1543 trebles[numZone] = minToneLevel;
1544 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1545 trebles[numZone] = maxToneLevel;
1547 trebles[numZone] = Integer.parseInt(value);
1550 updateChannelState(CHANNEL_TREBLE);
1551 updateChannelState(CHANNEL_MAIN_TREBLE);
1553 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1557 source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1558 sources[0] = source;
1559 updateChannelState(CHANNEL_SOURCE);
1560 updateChannelState(CHANNEL_MAIN_SOURCE);
1561 RotelCommand presetGetCmd = getRadioPresetGetCommand(source);
1562 if (presetGetCmd != null) {
1563 // Request current preset (with a delay)
1564 scheduler.schedule(() -> {
1566 sendCommand(presetGetCmd);
1567 } catch (RotelException e) {
1568 logger.debug("Getting the radio preset failed: {}", e.getMessage());
1570 }, 250, TimeUnit.MILLISECONDS);
1573 updateChannelState(CHANNEL_RADIO_PRESET);
1577 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1578 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1580 case KEY_SOURCE_ZONE2:
1581 case KEY_SOURCE_ZONE3:
1582 case KEY_SOURCE_ZONE4:
1583 case KEY_INPUT_ZONE1:
1584 case KEY_INPUT_ZONE2:
1585 case KEY_INPUT_ZONE3:
1586 case KEY_INPUT_ZONE4:
1587 sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
1588 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1591 if ("dolby_pliix_movie".equals(value)) {
1592 value = "dolby_plii_movie";
1593 } else if ("dolby_pliix_music".equals(value)) {
1594 value = "dolby_plii_music";
1595 } else if ("dolby_pliix_game".equals(value)) {
1596 value = "dolby_plii_game";
1598 dsp = model.getDspFromFeedback(value);
1599 logger.debug("DSP {}", dsp.getName());
1600 updateChannelState(CHANNEL_DSP);
1601 updateChannelState(CHANNEL_MAIN_DSP);
1603 case KEY1_PLAY_STATUS:
1604 case KEY2_PLAY_STATUS:
1605 if (PLAY.equalsIgnoreCase(value)) {
1606 playStatus = RotelPlayStatus.PLAYING;
1607 updateChannelState(CHANNEL_PLAY_CONTROL);
1608 } else if (PAUSE.equalsIgnoreCase(value)) {
1609 playStatus = RotelPlayStatus.PAUSED;
1610 updateChannelState(CHANNEL_PLAY_CONTROL);
1611 } else if (STOP.equalsIgnoreCase(value)) {
1612 playStatus = RotelPlayStatus.STOPPED;
1613 updateChannelState(CHANNEL_PLAY_CONTROL);
1615 throw new RotelException("Invalid value");
1619 source = sources[0];
1620 if (source != null && "CD".equals(source.getName()) && !model.hasSourceControl()) {
1621 track = Integer.parseInt(value);
1622 updateChannelState(CHANNEL_TRACK);
1626 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1628 updateChannelState(CHANNEL_RANDOM);
1629 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1631 updateChannelState(CHANNEL_RANDOM);
1633 throw new RotelException("Invalid value");
1637 if (TRACK.equalsIgnoreCase(value)) {
1638 repeatMode = RotelRepeatMode.TRACK;
1639 updateChannelState(CHANNEL_REPEAT);
1640 } else if (DISC.equalsIgnoreCase(value)) {
1641 repeatMode = RotelRepeatMode.DISC;
1642 updateChannelState(CHANNEL_REPEAT);
1643 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1644 repeatMode = RotelRepeatMode.OFF;
1645 updateChannelState(CHANNEL_REPEAT);
1647 throw new RotelException("Invalid value");
1651 case KEY_PRESET_DAB:
1652 case KEY_PRESET_IRADIO:
1653 preset = Integer.parseInt(value);
1655 case KEY_DAB_PRESET:
1656 case KEY_IRADIO_PRESET:
1657 if (preset >= 1 && preset <= 30) {
1658 radioPreset = preset;
1662 updateChannelState(CHANNEL_RADIO_PRESET);
1666 preset = Integer.parseInt(value);
1667 if (preset >= 1 && preset <= 30) {
1668 radioPreset = preset;
1669 updateChannelState(CHANNEL_RADIO_PRESET);
1673 case KEY_FREQ_ZONE1:
1674 case KEY_FREQ_ZONE2:
1675 case KEY_FREQ_ZONE3:
1676 case KEY_FREQ_ZONE4:
1677 if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
1678 frequencies[numZone] = 0.0;
1680 // Suppress a potential ending "k" or "K"
1681 if (value.toUpperCase().endsWith("K")) {
1682 value = value.substring(0, value.length() - 1);
1684 frequencies[numZone] = Double.parseDouble(value);
1687 updateChannelState(CHANNEL_FREQUENCY);
1689 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1693 brightness = Integer.parseInt(value);
1694 updateChannelState(CHANNEL_BRIGHTNESS);
1695 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1697 case KEY_UPDATE_MODE:
1698 case KEY_DISPLAY_UPDATE:
1701 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1703 updateChannelState(CHANNEL_TCBYPASS);
1704 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1706 updateChannelState(CHANNEL_TCBYPASS);
1708 throw new RotelException("Invalid value");
1712 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1714 updateChannelState(CHANNEL_TCBYPASS);
1715 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1717 updateChannelState(CHANNEL_TCBYPASS);
1719 throw new RotelException("Invalid value");
1723 case KEY_BALANCE_ZONE1:
1724 case KEY_BALANCE_ZONE2:
1725 case KEY_BALANCE_ZONE3:
1726 case KEY_BALANCE_ZONE4:
1727 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1728 balances[numZone] = minBalanceLevel;
1729 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1730 balances[numZone] = maxBalanceLevel;
1731 } else if (value.toUpperCase().startsWith("L")) {
1732 balances[numZone] = -Integer.parseInt(value.substring(1));
1733 } else if (value.toUpperCase().startsWith("R")) {
1734 balances[numZone] = Integer.parseInt(value.substring(1));
1736 balances[numZone] = Integer.parseInt(value);
1739 updateChannelState(CHANNEL_BALANCE);
1741 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1745 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1748 updateChannelState(CHANNEL_SPEAKER_A);
1749 updateChannelState(CHANNEL_SPEAKER_B);
1750 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1753 updateChannelState(CHANNEL_SPEAKER_A);
1754 updateChannelState(CHANNEL_SPEAKER_B);
1755 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1758 updateChannelState(CHANNEL_SPEAKER_A);
1759 updateChannelState(CHANNEL_SPEAKER_B);
1760 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1763 updateChannelState(CHANNEL_SPEAKER_A);
1764 updateChannelState(CHANNEL_SPEAKER_B);
1766 throw new RotelException("Invalid value");
1770 logger.debug("Sub level is set to {}", value);
1772 case KEY_CENTER_LEVEL:
1773 logger.debug("Center level is set to {}", value);
1775 case KEY_SURROUND_RIGHT_LEVEL:
1776 logger.debug("Surround right level is set to {}", value);
1778 case KEY_SURROUND_LEFT_LEVEL:
1779 logger.debug("Surround left level is set to {}", value);
1781 case KEY_CENTER_BACK_RIGHT_LEVEL:
1782 logger.debug("Center back right level is set to {}", value);
1784 case KEY_CENTER_BACK_LEFT_LEVEL:
1785 logger.debug("Center back left level is set to {}", value);
1787 case KEY_CEILING_FRONT_RIGHT_LEVEL:
1788 logger.debug("Ceiling front right level is set to {}", value);
1790 case KEY_CEILING_FRONT_LEFT_LEVEL:
1791 logger.debug("Ceiling front left level is set to {}", value);
1793 case KEY_CEILING_REAR_RIGHT_LEVEL:
1794 logger.debug("Ceiling rear right level is set to {}", value);
1796 case KEY_CEILING_REAR_LEFT_LEVEL:
1797 logger.debug("Ceiling rear left level is set to {}", value);
1799 case KEY_PCUSB_CLASS:
1800 logger.debug("PC-USB Audio Class is set to {}", value);
1802 case KEY_PRODUCT_TYPE:
1804 getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
1806 case KEY_PRODUCT_VERSION:
1808 getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
1811 logger.debug("onNewMessageEvent: unhandled key {}", key);
1814 } catch (NumberFormatException | RotelException e) {
1815 logger.debug("Invalid value {} for key {}", value, key);
1820 * Handle the received information that device power (main zone) is ON
1822 private void handlePowerOn() {
1823 Boolean prev = powers[0];
1825 updateChannelState(CHANNEL_POWER);
1826 updateChannelState(CHANNEL_MAIN_POWER);
1827 updateChannelState(CHANNEL_ALL_POWER);
1828 if ((prev == null) || !prev) {
1829 schedulePowerOnJob();
1834 * Handle the received information that device power (main zone) is OFF
1836 private void handlePowerOff() {
1837 cancelPowerOnZoneJob(0);
1839 updateChannelState(CHANNEL_POWER);
1840 updateChannelState(CHANNEL_SOURCE);
1841 updateChannelState(CHANNEL_DSP);
1842 updateChannelState(CHANNEL_VOLUME);
1843 updateChannelState(CHANNEL_MUTE);
1844 updateChannelState(CHANNEL_BASS);
1845 updateChannelState(CHANNEL_TREBLE);
1846 updateChannelState(CHANNEL_PLAY_CONTROL);
1847 updateChannelState(CHANNEL_TRACK);
1848 updateChannelState(CHANNEL_RANDOM);
1849 updateChannelState(CHANNEL_REPEAT);
1850 updateChannelState(CHANNEL_RADIO_PRESET);
1851 updateChannelState(CHANNEL_FREQUENCY);
1852 updateChannelState(CHANNEL_BRIGHTNESS);
1853 updateChannelState(CHANNEL_TCBYPASS);
1854 updateChannelState(CHANNEL_BALANCE);
1855 updateChannelState(CHANNEL_SPEAKER_A);
1856 updateChannelState(CHANNEL_SPEAKER_B);
1858 updateChannelState(CHANNEL_MAIN_POWER);
1859 updateChannelState(CHANNEL_MAIN_SOURCE);
1860 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1861 updateChannelState(CHANNEL_MAIN_DSP);
1862 updateChannelState(CHANNEL_MAIN_VOLUME);
1863 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1864 updateChannelState(CHANNEL_MAIN_MUTE);
1865 updateChannelState(CHANNEL_MAIN_BASS);
1866 updateChannelState(CHANNEL_MAIN_TREBLE);
1868 updateChannelState(CHANNEL_ALL_POWER);
1869 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1873 * Handle the received information that a zone power is ON
1875 private void handlePowerOnZone(int numZone) {
1876 Boolean prev = powers[numZone];
1877 powers[numZone] = true;
1878 updateGroupChannelState(numZone, CHANNEL_POWER);
1879 if ((prev == null) || !prev) {
1880 schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
1885 * Handle the received information that a zone power is OFF
1887 private void handlePowerOffZone(int numZone) {
1888 cancelPowerOnZoneJob(numZone);
1889 powers[numZone] = false;
1890 updateGroupChannelState(numZone, CHANNEL_POWER);
1891 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1892 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1893 updateGroupChannelState(numZone, CHANNEL_MUTE);
1894 updateGroupChannelState(numZone, CHANNEL_BASS);
1895 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1896 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1897 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1898 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1902 * Schedule the job that will consider the device as OFF if no new event is received before its running
1904 * @param switchOffAllZones true if all zones have to be considered as OFF
1906 private void schedulePowerOffJob(boolean switchOffAllZones) {
1907 logger.debug("Schedule power OFF job");
1908 cancelPowerOffJob();
1909 powerOffJob = scheduler.schedule(() -> {
1910 logger.debug("Power OFF job");
1912 if (switchOffAllZones) {
1913 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1914 handlePowerOffZone(zone);
1917 }, 2000, TimeUnit.MILLISECONDS);
1921 * Cancel the job that will consider the device as OFF
1923 private void cancelPowerOffJob() {
1924 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1925 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1926 powerOffJob.cancel(true);
1927 this.powerOffJob = null;
1932 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1934 private void schedulePowerOnJob() {
1935 logger.debug("Schedule power ON job");
1936 cancelPowerOnZoneJob(0);
1937 powerOnZoneJobs[0] = scheduler.schedule(() -> {
1938 synchronized (sequenceLock) {
1939 logger.debug("Power ON job");
1943 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1944 sendCommand(getVolumeDownCommand(0));
1946 sendCommand(getVolumeUpCommand(0));
1949 if (model.getNumberOfZones() > 1) {
1950 if (currentZone != 1
1951 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1952 selectZone(1, model.getZoneSelectCmd());
1953 } else if (!selectingRecord) {
1954 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1958 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1961 if (model.hasToneControl()) {
1962 if (model == RotelModel.RSX1065) {
1963 // No tone control select command
1964 sendCommand(RotelCommand.TREBLE_DOWN);
1966 sendCommand(RotelCommand.TREBLE_UP);
1968 sendCommand(RotelCommand.BASS_DOWN);
1970 sendCommand(RotelCommand.BASS_UP);
1973 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1978 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1979 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1980 sendCommand(RotelCommand.UPDATE_AUTO);
1981 Thread.sleep(SLEEP_INTV);
1983 if (model.hasSourceControl()) {
1984 sendCommand(RotelCommand.SOURCE);
1985 Thread.sleep(SLEEP_INTV);
1987 if (model.hasVolumeControl() || model.hasToneControl()) {
1988 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1989 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1990 sendCommand(RotelCommand.VOLUME_GET_MIN);
1991 Thread.sleep(SLEEP_INTV);
1992 sendCommand(RotelCommand.VOLUME_GET_MAX);
1993 Thread.sleep(SLEEP_INTV);
1995 if (model.hasToneControl()) {
1996 sendCommand(RotelCommand.TONE_MAX);
1997 Thread.sleep(SLEEP_INTV);
1999 // Wait enough to be sure to get the min/max values requested just before
2001 if (model.hasVolumeControl()) {
2002 sendCommand(RotelCommand.VOLUME_GET);
2003 Thread.sleep(SLEEP_INTV);
2004 if (model != RotelModel.RA11 && model != RotelModel.RA12
2005 && model != RotelModel.RCX1500) {
2006 sendCommand(RotelCommand.MUTE);
2007 Thread.sleep(SLEEP_INTV);
2010 if (model.hasToneControl()) {
2011 sendCommand(RotelCommand.BASS);
2012 Thread.sleep(SLEEP_INTV);
2013 sendCommand(RotelCommand.TREBLE);
2014 Thread.sleep(SLEEP_INTV);
2015 if (model.canGetBypassStatus()) {
2016 sendCommand(RotelCommand.TONE_CONTROLS);
2017 Thread.sleep(SLEEP_INTV);
2021 if (model.hasBalanceControl()) {
2022 sendCommand(RotelCommand.BALANCE);
2023 Thread.sleep(SLEEP_INTV);
2025 if (model.hasPlayControl()) {
2026 RotelSource source = sources[0];
2027 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
2028 && (model != RotelModel.RCX1500 || source == null
2029 || !"CD".equals(source.getName()))) {
2030 sendCommand(RotelCommand.PLAY_STATUS);
2031 Thread.sleep(SLEEP_INTV);
2033 sendCommand(RotelCommand.CD_PLAY_STATUS);
2034 Thread.sleep(SLEEP_INTV);
2037 if (model.hasDspControl()) {
2038 sendCommand(RotelCommand.DSP_MODE);
2039 Thread.sleep(SLEEP_INTV);
2041 if (model.canGetFrequency()) {
2042 sendCommand(RotelCommand.FREQUENCY);
2043 Thread.sleep(SLEEP_INTV);
2045 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
2046 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
2047 Thread.sleep(SLEEP_INTV);
2049 if (model.hasSpeakerGroups()) {
2050 sendCommand(RotelCommand.SPEAKER);
2051 Thread.sleep(SLEEP_INTV);
2053 if (model != RotelModel.RAP1580 && model != RotelModel.RSP1576
2054 && model != RotelModel.RSP1582) {
2055 sendCommand(RotelCommand.MODEL);
2056 Thread.sleep(SLEEP_INTV);
2057 sendCommand(RotelCommand.VERSION);
2058 Thread.sleep(SLEEP_INTV);
2062 sendCommand(RotelCommand.UPDATE_AUTO);
2063 Thread.sleep(SLEEP_INTV);
2064 if (model.hasSourceControl()) {
2065 if (model.getNumberOfZones() > 1) {
2066 sendCommand(RotelCommand.INPUT);
2068 sendCommand(RotelCommand.SOURCE);
2070 Thread.sleep(SLEEP_INTV);
2072 if (model.hasVolumeControl()) {
2073 sendCommand(RotelCommand.VOLUME_GET);
2074 Thread.sleep(SLEEP_INTV);
2075 sendCommand(RotelCommand.MUTE);
2076 Thread.sleep(SLEEP_INTV);
2078 if (model.hasToneControl()) {
2079 sendCommand(RotelCommand.BASS);
2080 Thread.sleep(SLEEP_INTV);
2081 sendCommand(RotelCommand.TREBLE);
2082 Thread.sleep(SLEEP_INTV);
2083 if (model.canGetBypassStatus()) {
2084 sendCommand(RotelCommand.TCBYPASS);
2085 Thread.sleep(SLEEP_INTV);
2088 if (model.hasBalanceControl()) {
2089 sendCommand(RotelCommand.BALANCE);
2090 Thread.sleep(SLEEP_INTV);
2092 if (model.hasPlayControl()) {
2093 sendCommand(RotelCommand.PLAY_STATUS);
2094 Thread.sleep(SLEEP_INTV);
2095 RotelSource source = sources[0];
2096 if (source != null && "CD".equals(source.getName()) && !model.hasSourceControl()) {
2097 sendCommand(RotelCommand.TRACK);
2098 Thread.sleep(SLEEP_INTV);
2099 sendCommand(RotelCommand.RANDOM_MODE);
2100 Thread.sleep(SLEEP_INTV);
2101 sendCommand(RotelCommand.REPEAT_MODE);
2102 Thread.sleep(SLEEP_INTV);
2105 if (model.hasDspControl()) {
2106 sendCommand(RotelCommand.DSP_MODE);
2107 Thread.sleep(SLEEP_INTV);
2109 if (model.canGetFrequency()) {
2110 sendCommand(RotelCommand.FREQUENCY);
2111 Thread.sleep(SLEEP_INTV);
2113 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
2114 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
2115 Thread.sleep(SLEEP_INTV);
2117 if (model.hasSpeakerGroups()) {
2118 sendCommand(RotelCommand.SPEAKER);
2119 Thread.sleep(SLEEP_INTV);
2121 sendCommand(RotelCommand.MODEL);
2122 Thread.sleep(SLEEP_INTV);
2123 sendCommand(RotelCommand.VERSION);
2124 Thread.sleep(SLEEP_INTV);
2127 } catch (RotelException e) {
2128 logger.debug("Init sequence failed: {}", e.getMessage());
2129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2130 "@text/offline.comm-error-init-sequence");
2132 } catch (InterruptedException e) {
2133 logger.debug("Init sequence interrupted: {}", e.getMessage());
2134 Thread.currentThread().interrupt();
2137 }, 2500, TimeUnit.MILLISECONDS);
2141 * Schedule the job to run with a few seconds delay when the zone power switched ON
2143 private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
2144 logger.debug("Schedule power ON zone {} job", numZone);
2145 cancelPowerOnZoneJob(numZone);
2146 powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
2147 synchronized (sequenceLock) {
2148 logger.debug("Power ON zone {} job", numZone);
2150 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
2151 selectZone(numZone, model.getZoneSelectCmd());
2152 sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
2154 sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
2157 } catch (RotelException e) {
2158 logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
2159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2160 String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
2162 } catch (InterruptedException e) {
2163 logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
2164 Thread.currentThread().interrupt();
2167 }, 2500, TimeUnit.MILLISECONDS);
2171 * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
2173 private void cancelPowerOnZoneJob(int numZone) {
2174 ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
2175 if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
2176 powerOnZoneJob.cancel(true);
2177 powerOnZoneJobs[numZone] = null;
2182 * Schedule the reconnection job
2184 private void scheduleReconnectJob() {
2185 logger.debug("Schedule reconnect job");
2186 cancelReconnectJob();
2187 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2188 if (!connector.isConnected()) {
2189 logger.debug("Trying to reconnect...");
2192 String error = null;
2193 if (openConnection()) {
2194 synchronized (sequenceLock) {
2195 schedulePowerOffJob(true);
2197 sendCommand(model.getPowerStateCmd());
2198 } catch (RotelException e) {
2199 error = "@text/offline.comm-error-first-command-after-reconnection";
2200 logger.debug("First command after connection failed", e);
2201 cancelPowerOffJob();
2206 error = "@text/offline.comm-error-reconnection";
2208 if (error != null) {
2210 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
2211 handlePowerOffZone(zone);
2213 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2215 updateStatus(ThingStatus.ONLINE);
2218 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2222 * Cancel the reconnection job
2224 private void cancelReconnectJob() {
2225 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2226 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2227 reconnectJob.cancel(true);
2228 this.reconnectJob = null;
2232 private void updateGroupChannelState(int numZone, String channel) {
2233 updateChannelState(String.format("zone%d#%s", numZone, channel));
2237 * Update the state of a channel
2239 * @param channel the channel
2241 private void updateChannelState(String channel) {
2242 if (!isLinked(channel)) {
2245 State state = UnDefType.UNDEF;
2246 RotelSource localSource;
2249 case CHANNEL_ZONE1_SOURCE:
2250 case CHANNEL_ZONE1_VOLUME:
2251 case CHANNEL_ZONE1_MUTE:
2252 case CHANNEL_ZONE1_BASS:
2253 case CHANNEL_ZONE1_TREBLE:
2254 case CHANNEL_ZONE1_BALANCE:
2255 case CHANNEL_ZONE1_FREQUENCY:
2258 case CHANNEL_ZONE2_POWER:
2259 case CHANNEL_ZONE2_SOURCE:
2260 case CHANNEL_ZONE2_VOLUME:
2261 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2262 case CHANNEL_ZONE2_MUTE:
2263 case CHANNEL_ZONE2_BASS:
2264 case CHANNEL_ZONE2_TREBLE:
2265 case CHANNEL_ZONE2_BALANCE:
2266 case CHANNEL_ZONE2_FREQUENCY:
2269 case CHANNEL_ZONE3_POWER:
2270 case CHANNEL_ZONE3_SOURCE:
2271 case CHANNEL_ZONE3_VOLUME:
2272 case CHANNEL_ZONE3_MUTE:
2273 case CHANNEL_ZONE3_BASS:
2274 case CHANNEL_ZONE3_TREBLE:
2275 case CHANNEL_ZONE3_BALANCE:
2276 case CHANNEL_ZONE3_FREQUENCY:
2279 case CHANNEL_ZONE4_POWER:
2280 case CHANNEL_ZONE4_SOURCE:
2281 case CHANNEL_ZONE4_VOLUME:
2282 case CHANNEL_ZONE4_MUTE:
2283 case CHANNEL_ZONE4_BASS:
2284 case CHANNEL_ZONE4_TREBLE:
2285 case CHANNEL_ZONE4_BALANCE:
2286 case CHANNEL_ZONE4_FREQUENCY:
2294 case CHANNEL_MAIN_POWER:
2295 case CHANNEL_ALL_POWER:
2296 case CHANNEL_ZONE2_POWER:
2297 case CHANNEL_ZONE3_POWER:
2298 case CHANNEL_ZONE4_POWER:
2299 Boolean powerZone = powers[numZone];
2300 if (powerZone != null) {
2301 state = OnOffType.from(powerZone.booleanValue());
2304 case CHANNEL_SOURCE:
2305 case CHANNEL_MAIN_SOURCE:
2306 case CHANNEL_ZONE1_SOURCE:
2307 case CHANNEL_ZONE2_SOURCE:
2308 case CHANNEL_ZONE3_SOURCE:
2309 case CHANNEL_ZONE4_SOURCE:
2310 localSource = sources[numZone];
2311 if (isPowerOn(numZone) && localSource != null) {
2312 state = new StringType(localSource.getName());
2315 case CHANNEL_MAIN_RECORD_SOURCE:
2316 localSource = recordSource;
2317 if (isPowerOn() && localSource != null) {
2318 state = new StringType(localSource.getName());
2322 case CHANNEL_MAIN_DSP:
2324 state = new StringType(dsp.getName());
2327 case CHANNEL_VOLUME:
2328 case CHANNEL_MAIN_VOLUME:
2329 case CHANNEL_ZONE1_VOLUME:
2330 case CHANNEL_ZONE2_VOLUME:
2331 case CHANNEL_ZONE3_VOLUME:
2332 case CHANNEL_ZONE4_VOLUME:
2333 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2334 long volumePct = Math
2335 .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2336 state = new PercentType(BigDecimal.valueOf(volumePct));
2339 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2340 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2341 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2342 state = new DecimalType(volumes[numZone]);
2346 case CHANNEL_MAIN_MUTE:
2347 case CHANNEL_ZONE1_MUTE:
2348 case CHANNEL_ZONE2_MUTE:
2349 case CHANNEL_ZONE3_MUTE:
2350 case CHANNEL_ZONE4_MUTE:
2351 if (isPowerOn(numZone)) {
2352 state = OnOffType.from(mutes[numZone]);
2356 case CHANNEL_MAIN_BASS:
2357 case CHANNEL_ZONE1_BASS:
2358 case CHANNEL_ZONE2_BASS:
2359 case CHANNEL_ZONE3_BASS:
2360 case CHANNEL_ZONE4_BASS:
2361 if (isPowerOn(numZone)) {
2362 state = new DecimalType(basses[numZone]);
2365 case CHANNEL_TREBLE:
2366 case CHANNEL_MAIN_TREBLE:
2367 case CHANNEL_ZONE1_TREBLE:
2368 case CHANNEL_ZONE2_TREBLE:
2369 case CHANNEL_ZONE3_TREBLE:
2370 case CHANNEL_ZONE4_TREBLE:
2371 if (isPowerOn(numZone)) {
2372 state = new DecimalType(trebles[numZone]);
2376 if (isPowerOn() && track > 0) {
2377 state = new DecimalType(track);
2380 case CHANNEL_RANDOM:
2382 state = OnOffType.from(randomMode);
2385 case CHANNEL_REPEAT:
2387 state = new StringType(repeatMode.name());
2390 case CHANNEL_PLAY_CONTROL:
2392 switch (playStatus) {
2394 state = PlayPauseType.PLAY;
2398 state = PlayPauseType.PAUSE;
2403 case CHANNEL_RADIO_PRESET:
2405 state = radioPreset == 0 ? UnDefType.UNDEF : new DecimalType(radioPreset);
2408 case CHANNEL_FREQUENCY:
2409 case CHANNEL_ZONE1_FREQUENCY:
2410 case CHANNEL_ZONE2_FREQUENCY:
2411 case CHANNEL_ZONE3_FREQUENCY:
2412 case CHANNEL_ZONE4_FREQUENCY:
2413 if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
2414 state = new DecimalType(frequencies[numZone]);
2418 state = new StringType(frontPanelLine1);
2421 state = new StringType(frontPanelLine2);
2423 case CHANNEL_BRIGHTNESS:
2424 case CHANNEL_ALL_BRIGHTNESS:
2425 if (isPowerOn() && model.hasDimmerControl()) {
2426 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2427 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2428 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2431 case CHANNEL_TCBYPASS:
2433 state = OnOffType.from(tcbypass);
2436 case CHANNEL_BALANCE:
2437 case CHANNEL_ZONE1_BALANCE:
2438 case CHANNEL_ZONE2_BALANCE:
2439 case CHANNEL_ZONE3_BALANCE:
2440 case CHANNEL_ZONE4_BALANCE:
2441 if (isPowerOn(numZone)) {
2442 state = new DecimalType(balances[numZone]);
2445 case CHANNEL_SPEAKER_A:
2447 state = OnOffType.from(speakera);
2450 case CHANNEL_SPEAKER_B:
2452 state = OnOffType.from(speakerb);
2458 updateState(channel, state);
2462 * Inform about the device / main zone power state
2464 * @return true if device / main zone power state is known and known as ON
2466 private boolean isPowerOn() {
2467 return isPowerOn(0);
2471 * Inform about the power state
2473 * @param numZone the zone number (1-4) or 0 for the device or main zone
2475 * @return true if power state is known and known as ON
2477 private boolean isPowerOn(int numZone) {
2478 if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
2479 throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
2481 Boolean power = powers[numZone];
2482 return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
2486 * Get the command to be used for POWER ON
2488 * @param numZone the zone number (2-4) or 0 for the device or main zone
2490 * @return the command
2492 private RotelCommand getPowerOnCommand(int numZone) {
2495 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2497 return RotelCommand.ZONE2_POWER_ON;
2499 return RotelCommand.ZONE3_POWER_ON;
2501 return RotelCommand.ZONE4_POWER_ON;
2503 throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
2508 * Get the command to be used for POWER OFF
2510 * @param numZone the zone number (2-4) or 0 for the device or main zone
2512 * @return the command
2514 private RotelCommand getPowerOffCommand(int numZone) {
2517 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2519 return RotelCommand.ZONE2_POWER_OFF;
2521 return RotelCommand.ZONE3_POWER_OFF;
2523 return RotelCommand.ZONE4_POWER_OFF;
2525 throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
2530 * Get the command to be used for VOLUME UP
2532 * @param numZone the zone number (1-4) or 0 for the device or main zone
2534 * @return the command
2536 private RotelCommand getVolumeUpCommand(int numZone) {
2539 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2541 return RotelCommand.ZONE1_VOLUME_UP;
2543 return RotelCommand.ZONE2_VOLUME_UP;
2545 return RotelCommand.ZONE3_VOLUME_UP;
2547 return RotelCommand.ZONE4_VOLUME_UP;
2549 throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
2554 * Get the command to be used for VOLUME DOWN
2556 * @param numZone the zone number (1-4) or 0 for the device or main zone
2558 * @return the command
2560 private RotelCommand getVolumeDownCommand(int numZone) {
2563 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2564 : RotelCommand.VOLUME_DOWN;
2566 return RotelCommand.ZONE1_VOLUME_DOWN;
2568 return RotelCommand.ZONE2_VOLUME_DOWN;
2570 return RotelCommand.ZONE3_VOLUME_DOWN;
2572 return RotelCommand.ZONE4_VOLUME_DOWN;
2574 throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
2579 * Get the command to be used for VOLUME SET
2581 * @param numZone the zone number (1-4) or 0 for the device
2583 * @return the command
2585 private RotelCommand getVolumeSetCommand(int numZone) {
2588 return RotelCommand.VOLUME_SET;
2590 return RotelCommand.ZONE1_VOLUME_SET;
2592 return RotelCommand.ZONE2_VOLUME_SET;
2594 return RotelCommand.ZONE3_VOLUME_SET;
2596 return RotelCommand.ZONE4_VOLUME_SET;
2598 throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
2603 * Get the command to be used for MUTE ON
2605 * @param numZone the zone number (1-4) or 0 for the device or main zone
2607 * @return the command
2609 private RotelCommand getMuteOnCommand(int numZone) {
2612 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2614 return RotelCommand.ZONE1_MUTE_ON;
2616 return RotelCommand.ZONE2_MUTE_ON;
2618 return RotelCommand.ZONE3_MUTE_ON;
2620 return RotelCommand.ZONE4_MUTE_ON;
2622 throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
2627 * Get the command to be used for MUTE OFF
2629 * @param numZone the zone number (1-4) or 0 for the device or main zone
2631 * @return the command
2633 private RotelCommand getMuteOffCommand(int numZone) {
2636 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2638 return RotelCommand.ZONE1_MUTE_OFF;
2640 return RotelCommand.ZONE2_MUTE_OFF;
2642 return RotelCommand.ZONE3_MUTE_OFF;
2644 return RotelCommand.ZONE4_MUTE_OFF;
2646 throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
2651 * Get the command to be used for MUTE TOGGLE
2653 * @param numZone the zone number (1-4) or 0 for the device or main zone
2655 * @return the command
2657 private RotelCommand getMuteToggleCommand(int numZone) {
2660 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2661 : RotelCommand.MUTE_TOGGLE;
2663 return RotelCommand.ZONE1_MUTE_TOGGLE;
2665 return RotelCommand.ZONE2_MUTE_TOGGLE;
2667 return RotelCommand.ZONE3_MUTE_TOGGLE;
2669 return RotelCommand.ZONE4_MUTE_TOGGLE;
2671 throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
2676 * Get the command to be used for BASS UP
2678 * @param numZone the zone number (1-4) or 0 for the device
2680 * @return the command
2682 private RotelCommand getBassUpCommand(int numZone) {
2685 return RotelCommand.BASS_UP;
2687 return RotelCommand.ZONE1_BASS_UP;
2689 return RotelCommand.ZONE2_BASS_UP;
2691 return RotelCommand.ZONE3_BASS_UP;
2693 return RotelCommand.ZONE4_BASS_UP;
2695 throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
2700 * Get the command to be used for BASS DOWN
2702 * @param numZone the zone number (1-4) or 0 for the device
2704 * @return the command
2706 private RotelCommand getBassDownCommand(int numZone) {
2709 return RotelCommand.BASS_DOWN;
2711 return RotelCommand.ZONE1_BASS_DOWN;
2713 return RotelCommand.ZONE2_BASS_DOWN;
2715 return RotelCommand.ZONE3_BASS_DOWN;
2717 return RotelCommand.ZONE4_BASS_DOWN;
2719 throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
2724 * Get the command to be used for BASS SET
2726 * @param numZone the zone number (1-4) or 0 for the device
2728 * @return the command
2730 private RotelCommand getBassSetCommand(int numZone) {
2733 return RotelCommand.BASS_SET;
2735 return RotelCommand.ZONE1_BASS_SET;
2737 return RotelCommand.ZONE2_BASS_SET;
2739 return RotelCommand.ZONE3_BASS_SET;
2741 return RotelCommand.ZONE4_BASS_SET;
2743 throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
2748 * Get the command to be used for TREBLE UP
2750 * @param numZone the zone number (1-4) or 0 for the device
2752 * @return the command
2754 private RotelCommand getTrebleUpCommand(int numZone) {
2757 return RotelCommand.TREBLE_UP;
2759 return RotelCommand.ZONE1_TREBLE_UP;
2761 return RotelCommand.ZONE2_TREBLE_UP;
2763 return RotelCommand.ZONE3_TREBLE_UP;
2765 return RotelCommand.ZONE4_TREBLE_UP;
2767 throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
2772 * Get the command to be used for TREBLE DOWN
2774 * @param numZone the zone number (1-4) or 0 for the device
2776 * @return the command
2778 private RotelCommand getTrebleDownCommand(int numZone) {
2781 return RotelCommand.TREBLE_DOWN;
2783 return RotelCommand.ZONE1_TREBLE_DOWN;
2785 return RotelCommand.ZONE2_TREBLE_DOWN;
2787 return RotelCommand.ZONE3_TREBLE_DOWN;
2789 return RotelCommand.ZONE4_TREBLE_DOWN;
2791 throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
2796 * Get the command to be used for TREBLE SET
2798 * @param numZone the zone number (1-4) or 0 for the device
2800 * @return the command
2802 private RotelCommand getTrebleSetCommand(int numZone) {
2805 return RotelCommand.TREBLE_SET;
2807 return RotelCommand.ZONE1_TREBLE_SET;
2809 return RotelCommand.ZONE2_TREBLE_SET;
2811 return RotelCommand.ZONE3_TREBLE_SET;
2813 return RotelCommand.ZONE4_TREBLE_SET;
2815 throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
2820 * Get the command to be used for BALANCE LEFT
2822 * @param numZone the zone number (1-4) or 0 for the device
2824 * @return the command
2826 private RotelCommand getBalanceLeftCommand(int numZone) {
2829 return RotelCommand.BALANCE_LEFT;
2831 return RotelCommand.ZONE1_BALANCE_LEFT;
2833 return RotelCommand.ZONE2_BALANCE_LEFT;
2835 return RotelCommand.ZONE3_BALANCE_LEFT;
2837 return RotelCommand.ZONE4_BALANCE_LEFT;
2839 throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
2844 * Get the command to be used for BALANCE RIGHT
2846 * @param numZone the zone number (1-4) or 0 for the device
2848 * @return the command
2850 private RotelCommand getBalanceRightCommand(int numZone) {
2853 return RotelCommand.BALANCE_RIGHT;
2855 return RotelCommand.ZONE1_BALANCE_RIGHT;
2857 return RotelCommand.ZONE2_BALANCE_RIGHT;
2859 return RotelCommand.ZONE3_BALANCE_RIGHT;
2861 return RotelCommand.ZONE4_BALANCE_RIGHT;
2863 throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
2868 * Get the command to be used for BALANCE SET
2870 * @param numZone the zone number (1-4) or 0 for the device
2872 * @return the command
2874 private RotelCommand getBalanceSetCommand(int numZone) {
2877 return RotelCommand.BALANCE_SET;
2879 return RotelCommand.ZONE1_BALANCE_SET;
2881 return RotelCommand.ZONE2_BALANCE_SET;
2883 return RotelCommand.ZONE3_BALANCE_SET;
2885 return RotelCommand.ZONE4_BALANCE_SET;
2887 throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
2891 private @Nullable RotelCommand getRadioPresetGetCommand(RotelSource source) {
2892 if (protocol == RotelProtocol.ASCII_V1) {
2893 switch (source.getName()) {
2897 return RotelCommand.PRESET;
2901 } else if (protocol == RotelProtocol.ASCII_V2) {
2902 switch (source.getName()) {
2904 return RotelCommand.FM_PRESET;
2906 return RotelCommand.DAB_PRESET;
2914 private @Nullable RotelCommand getRadioPresetCallCommand(RotelSource source) {
2915 switch (source.getName()) {
2917 return RotelCommand.CALL_FM_PRESET;
2919 return RotelCommand.CALL_DAB_PRESET;
2921 return RotelCommand.CALL_IRADIO_PRESET;
2928 private void sendCommand(RotelCommand cmd) throws RotelException {
2929 sendCommand(cmd, null);
2933 * Request the Rotel device to execute a command
2935 * @param cmd the command to execute
2936 * @param value the integer value to consider for volume, bass or treble adjustment
2938 * @throws RotelException - In case of any problem
2940 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2943 message = protocolHandler.buildCommandMessage(cmd, value);
2944 } catch (RotelException e) {
2945 // Command not supported
2946 logger.debug("sendCommand: {}", e.getMessage());
2949 connector.writeOutput(cmd, message);
2951 if (connector instanceof RotelSimuConnector simuConnector) {
2952 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2953 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2954 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2955 simuConnector.buildFeedbackMessage(cmd, value);