2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.rotel.internal.handler;
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.EventObject;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelBindingConstants;
29 import org.openhab.binding.rotel.internal.RotelException;
30 import org.openhab.binding.rotel.internal.RotelModel;
31 import org.openhab.binding.rotel.internal.RotelPlayStatus;
32 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
33 import org.openhab.binding.rotel.internal.communication.RotelCommand;
34 import org.openhab.binding.rotel.internal.communication.RotelConnector;
35 import org.openhab.binding.rotel.internal.communication.RotelDsp;
36 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
38 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSource;
40 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
41 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
42 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
43 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
44 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
45 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
46 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
47 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
48 import org.openhab.core.io.transport.serial.SerialPortManager;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.IncreaseDecreaseType;
51 import org.openhab.core.library.types.NextPreviousType;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.PercentType;
54 import org.openhab.core.library.types.PlayPauseType;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.StateOption;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
72 * @author Laurent Garnier - Initial contribution
75 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
77 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
79 private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
80 private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
81 private static final boolean USE_SIMULATED_DEVICE = false;
82 private static final int SLEEP_INTV = 30;
84 private @Nullable ScheduledFuture<?> reconnectJob;
85 private @Nullable ScheduledFuture<?> powerOffJob;
86 private @Nullable ScheduledFuture<?>[] powerOnZoneJobs = { null, null, null, null, null };
88 private RotelStateDescriptionOptionProvider stateDescriptionProvider;
89 private SerialPortManager serialPortManager;
91 private RotelModel model;
92 private RotelProtocol protocol;
93 private RotelAbstractProtocolHandler protocolHandler;
94 private RotelConnector connector;
96 private int minVolume;
97 private int maxVolume;
98 private int minToneLevel;
99 private int maxToneLevel;
101 private int currentZone = 1;
102 private boolean selectingRecord;
103 private @Nullable Boolean[] powers = { null, false, false, false, false };
104 private boolean powerControlPerZone;
105 private @Nullable RotelSource recordSource;
106 private @Nullable RotelSource[] sources = { RotelSource.CAT0_CD, null, null, null, null };
107 private RotelDsp dsp = RotelDsp.CAT1_NONE;
108 private boolean[] fixedVolumeZones = { false, false, false, false, false };
109 private int[] volumes = { 0, 0, 0, 0, 0 };
110 private boolean[] mutes = { false, false, false, false, false };
111 private int[] basses = { 0, 0, 0, 0, 0 };
112 private int[] trebles = { 0, 0, 0, 0, 0 };
113 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
115 private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
116 private String frontPanelLine1 = "";
117 private String frontPanelLine2 = "";
118 private int brightness;
119 private boolean tcbypass;
120 private int[] balances = { 0, 0, 0, 0, 0 };
121 private int minBalanceLevel;
122 private int maxBalanceLevel;
123 private boolean speakera;
124 private boolean speakerb;
126 private Object sequenceLock = new Object();
131 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
132 SerialPortManager serialPortManager) {
134 this.stateDescriptionProvider = stateDescriptionProvider;
135 this.serialPortManager = serialPortManager;
136 this.model = DEFAULT_MODEL;
137 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
138 this.protocol = protocolHandler.getProtocol();
139 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
143 public void initialize() {
144 logger.debug("Start initializing handler for thing {}", getThing().getUID());
146 switch (getThing().getThingTypeUID().getId()) {
147 case THING_TYPE_ID_RSP1066:
148 model = RotelModel.RSP1066;
150 case THING_TYPE_ID_RSP1068:
151 model = RotelModel.RSP1068;
153 case THING_TYPE_ID_RSP1069:
154 model = RotelModel.RSP1069;
156 case THING_TYPE_ID_RSP1098:
157 model = RotelModel.RSP1098;
159 case THING_TYPE_ID_RSP1570:
160 model = RotelModel.RSP1570;
162 case THING_TYPE_ID_RSP1572:
163 model = RotelModel.RSP1572;
165 case THING_TYPE_ID_RSX1055:
166 model = RotelModel.RSX1055;
168 case THING_TYPE_ID_RSX1056:
169 model = RotelModel.RSX1056;
171 case THING_TYPE_ID_RSX1057:
172 model = RotelModel.RSX1057;
174 case THING_TYPE_ID_RSX1058:
175 model = RotelModel.RSX1058;
177 case THING_TYPE_ID_RSX1065:
178 model = RotelModel.RSX1065;
180 case THING_TYPE_ID_RSX1067:
181 model = RotelModel.RSX1067;
183 case THING_TYPE_ID_RSX1550:
184 model = RotelModel.RSX1550;
186 case THING_TYPE_ID_RSX1560:
187 model = RotelModel.RSX1560;
189 case THING_TYPE_ID_RSX1562:
190 model = RotelModel.RSX1562;
192 case THING_TYPE_ID_A11:
193 model = RotelModel.A11;
195 case THING_TYPE_ID_A12:
196 model = RotelModel.A12;
198 case THING_TYPE_ID_A14:
199 model = RotelModel.A14;
201 case THING_TYPE_ID_CD11:
202 model = RotelModel.CD11;
204 case THING_TYPE_ID_CD14:
205 model = RotelModel.CD14;
207 case THING_TYPE_ID_RA11:
208 model = RotelModel.RA11;
210 case THING_TYPE_ID_RA12:
211 model = RotelModel.RA12;
213 case THING_TYPE_ID_RA1570:
214 model = RotelModel.RA1570;
216 case THING_TYPE_ID_RA1572:
217 model = RotelModel.RA1572;
219 case THING_TYPE_ID_RA1592:
220 model = RotelModel.RA1592;
222 case THING_TYPE_ID_RAP1580:
223 model = RotelModel.RAP1580;
225 case THING_TYPE_ID_RC1570:
226 model = RotelModel.RC1570;
228 case THING_TYPE_ID_RC1572:
229 model = RotelModel.RC1572;
231 case THING_TYPE_ID_RC1590:
232 model = RotelModel.RC1590;
234 case THING_TYPE_ID_RCD1570:
235 model = RotelModel.RCD1570;
237 case THING_TYPE_ID_RCD1572:
238 model = RotelModel.RCD1572;
240 case THING_TYPE_ID_RCX1500:
241 model = RotelModel.RCX1500;
243 case THING_TYPE_ID_RDD1580:
244 model = RotelModel.RDD1580;
246 case THING_TYPE_ID_RDG1520:
247 case THING_TYPE_ID_RT09:
248 model = RotelModel.RDG1520;
250 case THING_TYPE_ID_RSP1576:
251 model = RotelModel.RSP1576;
253 case THING_TYPE_ID_RSP1582:
254 model = RotelModel.RSP1582;
256 case THING_TYPE_ID_RT11:
257 model = RotelModel.RT11;
259 case THING_TYPE_ID_RT1570:
260 model = RotelModel.RT1570;
262 case THING_TYPE_ID_T11:
263 model = RotelModel.T11;
265 case THING_TYPE_ID_T14:
266 model = RotelModel.T14;
268 case THING_TYPE_ID_C8:
269 model = RotelModel.C8;
271 case THING_TYPE_ID_M8:
272 model = RotelModel.M8;
274 case THING_TYPE_ID_P5:
275 model = RotelModel.P5;
277 case THING_TYPE_ID_S5:
278 model = RotelModel.S5;
280 case THING_TYPE_ID_X3:
281 model = RotelModel.X3;
283 case THING_TYPE_ID_X5:
284 model = RotelModel.X5;
287 model = DEFAULT_MODEL;
291 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
293 protocol = RotelProtocol.HEX;
294 if (config.protocol != null && !config.protocol.isEmpty()) {
296 protocol = RotelProtocol.getFromName(config.protocol);
297 } catch (RotelException e) {
298 // Invalid protocol name in configuration, HEX will be considered by default
301 Map<String, String> properties = editProperties();
302 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
303 if (property != null && !property.isEmpty()) {
305 protocol = RotelProtocol.getFromName(property);
306 } catch (RotelException e) {
307 // Invalid protocol name in thing property, HEX will be considered by default
311 logger.debug("rotelProtocol {}", protocol.getName());
313 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
314 Map<RotelSource, String> sourcesLabels = new HashMap<>();
316 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
318 if (model.hasVolumeControl()) {
319 maxVolume = model.getVolumeMax();
320 if (!model.hasDirectVolumeControl()) {
322 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
323 minVolume, maxVolume);
326 if (model.hasToneControl()) {
327 maxToneLevel = model.getToneLevelMax();
328 minToneLevel = -maxToneLevel;
330 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
331 minToneLevel, maxToneLevel);
333 if (model.hasBalanceControl()) {
334 maxBalanceLevel = model.getBalanceLevelMax();
335 minBalanceLevel = -maxBalanceLevel;
336 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
337 minBalanceLevel, maxBalanceLevel);
340 powerControlPerZone = model.hasPowerControlPerZone();
342 // Check configuration settings
343 String configError = null;
344 if ((config.serialPort == null || config.serialPort.isEmpty())
345 && (config.host == null || config.host.isEmpty())) {
346 configError = "@text/offline.config-error-unknown-serialport-and-host";
347 } else if (config.host == null || config.host.isEmpty()) {
348 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
349 configError = "@text/offline.config-error-invalid-serial-over-ip";
352 if (config.port == null) {
353 configError = "@text/offline.config-error-unknown-port";
354 } else if (config.port <= 0) {
355 configError = "@text/offline.config-error-invalid-port";
359 if (configError != null) {
360 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
362 for (RotelSource src : model.getSources()) {
363 // Consider custom input labels
365 switch (src.getName()) {
367 label = config.inputLabelCd;
370 label = config.inputLabelTuner;
373 label = config.inputLabelTape;
376 label = config.inputLabelPhono;
379 label = config.inputLabelVideo1;
382 label = config.inputLabelVideo2;
385 label = config.inputLabelVideo3;
388 label = config.inputLabelVideo4;
391 label = config.inputLabelVideo5;
394 label = config.inputLabelVideo6;
397 label = config.inputLabelUsb;
400 label = config.inputLabelMulti;
405 if (label != null && !label.isEmpty()) {
406 sourcesCustomLabels.put(src, label);
408 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
411 if (protocol == RotelProtocol.HEX) {
412 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
413 } else if (protocol == RotelProtocol.ASCII_V1) {
414 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
416 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
419 if (USE_SIMULATED_DEVICE) {
420 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
421 } else if (config.serialPort != null) {
422 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
423 protocolHandler, readerThreadName);
425 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
428 if (model.hasSourceControl()) {
429 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
430 getStateOptions(model.getSources(), sourcesCustomLabels));
431 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
432 getStateOptions(model.getSources(), sourcesCustomLabels));
433 stateDescriptionProvider.setStateOptions(
434 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
435 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
437 if (model.hasZoneSourceControl(1)) {
438 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
439 getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
441 if (model.hasZoneSourceControl(2)) {
442 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
443 getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
445 if (model.hasZoneSourceControl(3)) {
446 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
447 getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
449 if (model.hasZoneSourceControl(4)) {
450 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
451 getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
453 if (model.hasDspControl()) {
454 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
455 model.getDspStateOptions());
456 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
457 model.getDspStateOptions());
460 updateStatus(ThingStatus.UNKNOWN);
462 scheduleReconnectJob();
465 logger.debug("Finished initializing!");
469 public void dispose() {
470 logger.debug("Disposing handler for thing {}", getThing().getUID());
472 for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
473 cancelPowerOnZoneJob(zone);
475 cancelReconnectJob();
480 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
481 List<StateOption> options = new ArrayList<>();
482 for (RotelSource item : list) {
483 String label = sourcesLabels.get(item);
484 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
490 public void handleCommand(ChannelUID channelUID, Command command) {
491 String channel = channelUID.getId();
493 if (getThing().getStatus() != ThingStatus.ONLINE) {
494 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
498 if (command instanceof RefreshType) {
499 updateChannelState(channel);
503 if (!connector.isConnected()) {
504 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
510 case CHANNEL_ZONE1_SOURCE:
511 case CHANNEL_ZONE1_VOLUME:
512 case CHANNEL_ZONE1_MUTE:
513 case CHANNEL_ZONE1_BASS:
514 case CHANNEL_ZONE1_TREBLE:
515 case CHANNEL_ZONE1_BALANCE:
518 case CHANNEL_ZONE2_POWER:
519 case CHANNEL_ZONE2_SOURCE:
520 case CHANNEL_ZONE2_VOLUME:
521 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
522 case CHANNEL_ZONE2_MUTE:
523 case CHANNEL_ZONE2_BASS:
524 case CHANNEL_ZONE2_TREBLE:
525 case CHANNEL_ZONE2_BALANCE:
528 case CHANNEL_ZONE3_POWER:
529 case CHANNEL_ZONE3_SOURCE:
530 case CHANNEL_ZONE3_VOLUME:
531 case CHANNEL_ZONE3_MUTE:
532 case CHANNEL_ZONE3_BASS:
533 case CHANNEL_ZONE3_TREBLE:
534 case CHANNEL_ZONE3_BALANCE:
537 case CHANNEL_ZONE4_POWER:
538 case CHANNEL_ZONE4_SOURCE:
539 case CHANNEL_ZONE4_VOLUME:
540 case CHANNEL_ZONE4_MUTE:
541 case CHANNEL_ZONE4_BASS:
542 case CHANNEL_ZONE4_TREBLE:
543 case CHANNEL_ZONE4_BALANCE:
552 boolean success = true;
553 synchronized (sequenceLock) {
557 case CHANNEL_MAIN_POWER:
558 case CHANNEL_ZONE2_POWER:
559 case CHANNEL_ZONE3_POWER:
560 case CHANNEL_ZONE4_POWER:
561 if (numZone == 0 || model.hasZoneCommands(numZone)) {
562 handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
563 } else if (numZone == 2 && model.getNumberOfZones() == 2) {
564 if (isPowerOn() || isPowerOn(numZone)) {
565 selectZone(2, model.getZoneSelectCmd());
567 sendCommand(RotelCommand.ZONE_SELECT);
570 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
573 case CHANNEL_ALL_POWER:
574 handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
577 case CHANNEL_MAIN_SOURCE:
578 case CHANNEL_ZONE1_SOURCE:
579 case CHANNEL_ZONE2_SOURCE:
580 case CHANNEL_ZONE3_SOURCE:
581 case CHANNEL_ZONE4_SOURCE:
582 if (!isPowerOn(numZone)) {
584 logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
585 numZone == 0 ? "device" : "zone " + numZone);
586 } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
587 src = model.getSourceFromName(command.toString());
589 cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
591 cmd = src.getZoneCommand(numZone);
595 if (model.canGetFrequency()) {
596 // send <new-source> returns
597 // 1.) the selected <new-source>
598 // 2.) the used frequency
600 // at response-time the frequency has the value of <old-source>
601 // so we must wait a short moment to get the frequency of <new-source>
603 sendCommand(RotelCommand.FREQUENCY);
605 updateChannelState(CHANNEL_FREQUENCY);
609 logger.debug("Command {} from channel {} failed: undefined source command", command,
612 } else if (numZone == 2 && model.getNumberOfZones() > 1) {
613 src = model.getSourceFromName(command.toString());
614 cmd = src.getCommand();
616 selectZone(2, model.getZoneSelectCmd());
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,
637 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
640 case CHANNEL_MAIN_RECORD_SOURCE:
643 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
644 } else if (model.hasOtherThanPrimaryCommands()) {
645 src = model.getSourceFromName(command.toString());
646 cmd = src.getRecordCommand();
651 logger.debug("Command {} from channel {} failed: undefined record source command",
655 src = model.getSourceFromName(command.toString());
656 cmd = src.getCommand();
658 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
663 logger.debug("Command {} from channel {} failed: undefined source command", command,
669 case CHANNEL_MAIN_DSP:
672 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
674 sendCommand(model.getCommandFromDspName(command.toString()));
678 case CHANNEL_MAIN_VOLUME:
679 case CHANNEL_MAIN_VOLUME_UP_DOWN:
680 case CHANNEL_ZONE1_VOLUME:
681 case CHANNEL_ZONE2_VOLUME:
682 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
683 case CHANNEL_ZONE3_VOLUME:
684 case CHANNEL_ZONE4_VOLUME:
685 if (!isPowerOn(numZone)) {
687 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
688 numZone == 0 ? "device" : "zone " + numZone);
689 } else if (fixedVolumeZones[numZone]) {
691 logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
692 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
693 handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
694 getVolumeDownCommand(numZone),
695 CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
696 || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
697 : getVolumeSetCommand(numZone));
698 } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
699 selectZone(2, model.getZoneSelectCmd());
700 handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
701 RotelCommand.VOLUME_DOWN,
702 CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
705 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
709 case CHANNEL_MAIN_MUTE:
710 case CHANNEL_ZONE1_MUTE:
711 case CHANNEL_ZONE2_MUTE:
712 case CHANNEL_ZONE3_MUTE:
713 case CHANNEL_ZONE4_MUTE:
714 if (!isPowerOn(numZone)) {
716 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
717 numZone == 0 ? "device" : "zone " + numZone);
718 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
719 handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
720 getMuteOnCommand(numZone), getMuteOffCommand(numZone),
721 getMuteToggleCommand(numZone));
724 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
728 case CHANNEL_MAIN_BASS:
729 case CHANNEL_ZONE1_BASS:
730 case CHANNEL_ZONE2_BASS:
731 case CHANNEL_ZONE3_BASS:
732 case CHANNEL_ZONE4_BASS:
733 if (!isPowerOn(numZone)) {
735 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
736 numZone == 0 ? "device" : "zone " + numZone);
737 } else if (tcbypass) {
739 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
741 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
742 handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
743 getBassDownCommand(numZone), getBassSetCommand(numZone));
746 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
750 case CHANNEL_MAIN_TREBLE:
751 case CHANNEL_ZONE1_TREBLE:
752 case CHANNEL_ZONE2_TREBLE:
753 case CHANNEL_ZONE3_TREBLE:
754 case CHANNEL_ZONE4_TREBLE:
755 if (!isPowerOn(numZone)) {
757 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
758 numZone == 0 ? "device" : "zone " + numZone);
759 } else if (tcbypass) {
761 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
763 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
764 handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
765 getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
768 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
771 case CHANNEL_PLAY_CONTROL:
774 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
775 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
776 sendCommand(RotelCommand.PLAY);
777 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
778 sendCommand(RotelCommand.PAUSE);
779 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
780 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
781 Thread.sleep(SLEEP_INTV);
782 sendCommand(RotelCommand.PLAY_STATUS);
784 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
785 sendCommand(RotelCommand.TRACK_FORWARD);
786 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
787 sendCommand(RotelCommand.TRACK_BACKWORD);
790 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
793 case CHANNEL_BRIGHTNESS:
794 case CHANNEL_ALL_BRIGHTNESS:
797 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
798 } else if (!model.hasDimmerControl()) {
800 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
801 } else if (command instanceof PercentType) {
802 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
803 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
804 + model.getDimmerLevelMin();
805 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
808 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
811 case CHANNEL_TCBYPASS:
814 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
815 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
817 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
819 handleTcbypassCmd(channel, command,
820 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
821 : RotelCommand.TCBYPASS_ON,
822 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
823 : RotelCommand.TCBYPASS_OFF);
826 case CHANNEL_BALANCE:
827 case CHANNEL_ZONE1_BALANCE:
828 case CHANNEL_ZONE2_BALANCE:
829 case CHANNEL_ZONE3_BALANCE:
830 case CHANNEL_ZONE4_BALANCE:
831 if (!isPowerOn(numZone)) {
833 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
834 numZone == 0 ? "device" : "zone " + numZone);
835 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
837 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
839 handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
840 getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
843 case CHANNEL_SPEAKER_A:
846 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
848 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
849 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
852 case CHANNEL_SPEAKER_B:
855 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
857 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
858 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
863 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
867 logger.debug("Command {} from channel {} succeeded", command, channel);
869 updateChannelState(channel);
871 } catch (RotelException e) {
872 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
873 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
874 "@text/offline.comm-error-sending-command");
876 scheduleReconnectJob();
877 } catch (InterruptedException e) {
878 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
879 Thread.currentThread().interrupt();
885 * Handle a power ON/OFF command
887 * @param channel the channel
888 * @param command the received channel command (OnOffType)
889 * @param onCmd the command to be sent to the device to power it ON
890 * @param offCmd the command to be sent to the device to power it OFF
892 * @throws RotelException in case of communication error with the device
894 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
895 throws RotelException {
896 if (command instanceof OnOffType && command == OnOffType.ON) {
898 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
901 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
906 * Handle a volume command
908 * @param current the current volume
909 * @param channel the channel
910 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
911 * @param upCmd the command to be sent to the device to increase the volume
912 * @param downCmd the command to be sent to the device to decrease the volume
913 * @param setCmd the command to be sent to the device to set the volume at a value
915 * @throws RotelException in case of communication error with the device
917 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
918 @Nullable RotelCommand setCmd) throws RotelException {
919 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
921 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
922 sendCommand(downCmd);
923 } else if (command instanceof DecimalType && setCmd == null) {
924 int value = ((DecimalType) command).intValue();
925 if (value >= minVolume && value <= maxVolume) {
926 if (value > current) {
928 } else if (value < current) {
929 sendCommand(downCmd);
932 } else if (command instanceof PercentType && setCmd != null) {
933 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
935 sendCommand(setCmd, value);
937 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
942 * Handle a mute command
944 * @param onlyToggle true if only the toggle command must be used
945 * @param channel the channel
946 * @param command the received channel command (OnOffType)
947 * @param onCmd the command to be sent to the device to mute
948 * @param offCmd the command to be sent to the device to unmute
949 * @param toggleCmd the command to be sent to the device to toggle the mute state
951 * @throws RotelException in case of communication error with the device
953 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
954 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
955 if (command instanceof OnOffType) {
957 sendCommand(toggleCmd);
958 } else if (command == OnOffType.ON) {
960 } else if (command == OnOffType.OFF) {
964 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
969 * Handle a tone level adjustment command (bass or treble)
971 * @param current the current tone level
972 * @param channel the channel
973 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
974 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
975 * @param upCmd the command to be sent to the device to increase the tone level
976 * @param downCmd the command to be sent to the device to decrease the tone level
977 * @param setCmd the command to be sent to the device to set the tone level at a value
979 * @throws RotelException in case of communication error with the device
980 * @throws InterruptedException in case of interruption during a thread sleep
982 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
983 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
984 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
985 selectToneControl(nbSelect);
987 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
988 selectToneControl(nbSelect);
989 sendCommand(downCmd);
990 } else if (command instanceof DecimalType) {
991 int value = ((DecimalType) command).intValue();
992 if (value >= minToneLevel && value <= maxToneLevel) {
993 if (protocol != RotelProtocol.HEX) {
994 sendCommand(setCmd, value);
995 } else if (value > current) {
996 selectToneControl(nbSelect);
998 } else if (value < current) {
999 selectToneControl(nbSelect);
1000 sendCommand(downCmd);
1004 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1009 * Handle a tcbypass command (only for ASCII protocol)
1011 * @param channel the channel
1012 * @param command the received channel command (OnOffType)
1013 * @param onCmd the command to be sent to the device to bypass_on
1014 * @param offCmd the command to be sent to the device to bypass_off
1016 * @throws RotelException in case of communication error with the device
1018 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1019 throws RotelException, InterruptedException {
1020 if (command instanceof OnOffType) {
1021 if (command == OnOffType.ON) {
1025 updateChannelState(CHANNEL_BASS);
1026 updateChannelState(CHANNEL_TREBLE);
1027 } else if (command == OnOffType.OFF) {
1028 sendCommand(offCmd);
1030 sendCommand(RotelCommand.BASS);
1032 sendCommand(RotelCommand.TREBLE);
1035 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1040 * Handle a speaker command
1042 * @param onlyToggle true if only the toggle command must be used
1043 * @param channel the channel
1044 * @param command the received channel command (OnOffType)
1045 * @param onCmd the command to be sent to the device to speaker_x_on
1046 * @param offCmd the command to be sent to the device to speaker_x_off
1047 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1049 * @throws RotelException in case of communication error with the device
1051 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1052 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1053 if (command instanceof OnOffType) {
1055 sendCommand(toggleCmd);
1056 } else if (command == OnOffType.ON) {
1058 } else if (command == OnOffType.OFF) {
1059 sendCommand(offCmd);
1062 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1067 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1069 * @param channel the channel
1070 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1071 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1072 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1073 * @param setCmd the command to be sent to the device to set the balance at a value
1075 * @throws RotelException in case of communication error with the device
1076 * @throws InterruptedException in case of interruption during a thread sleep
1078 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1079 RotelCommand setCmd) throws RotelException, InterruptedException {
1080 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1081 sendCommand(rightCmd);
1082 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1083 sendCommand(leftCmd);
1084 } else if (command instanceof DecimalType) {
1085 int value = ((DecimalType) command).intValue();
1086 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1087 sendCommand(setCmd, value);
1090 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1095 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1097 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1099 * @throws RotelException in case of communication error with the device
1100 * @throws InterruptedException in case of interruption during a thread sleep
1102 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1103 // No tone control select command for RSX-1065
1104 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1105 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1110 * Run a sequence of commands to display a particular zone on the device front panel
1112 * @param zone the zone to be displayed (1 for main zone)
1113 * @param selectCommand the command to be sent to the device to switch the display between zones
1115 * @throws RotelException in case of communication error with the device
1116 * @throws InterruptedException in case of interruption during a thread sleep
1118 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1119 throws RotelException, InterruptedException {
1120 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
1121 && selectCommand != null) {
1123 if (zone < currentZone) {
1124 nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
1125 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1129 nbSelect = zone - currentZone;
1130 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1131 && !selectingRecord) {
1135 selectFeature(nbSelect, null, selectCommand);
1140 * Run a sequence of commands to display a particular feature on the device front panel
1142 * @param nbSelect the number of select commands to be run
1143 * @param preCmd the initial command to be sent to the device (before the select commands)
1144 * @param selectCmd the select command to be sent to the device
1146 * @throws RotelException in case of communication error with the device
1147 * @throws InterruptedException in case of interruption during a thread sleep
1149 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1150 throws RotelException, InterruptedException {
1151 if (protocol == RotelProtocol.HEX) {
1152 if (preCmd != null) {
1153 sendCommand(preCmd);
1156 for (int i = 1; i <= nbSelect; i++) {
1157 sendCommand(selectCmd);
1164 * Open the connection with the Rotel device
1166 * @return true if the connection is opened successfully or flase if not
1168 private synchronized boolean openConnection() {
1169 protocolHandler.addEventListener(this);
1172 } catch (RotelException e) {
1173 logger.debug("openConnection() failed", e);
1175 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1176 return connector.isConnected();
1180 * Close the connection with the Rotel device
1182 private synchronized void closeConnection() {
1184 protocolHandler.removeEventListener(this);
1185 logger.debug("closeConnection(): disconnected");
1189 public void onNewMessageEvent(EventObject event) {
1190 cancelPowerOffJob();
1192 RotelMessageEvent evt = (RotelMessageEvent) event;
1193 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1195 String key = evt.getKey();
1196 String value = evt.getValue().trim();
1197 if (!KEY_ERROR.equals(key)) {
1198 updateStatus(ThingStatus.ONLINE);
1202 case KEY_INPUT_ZONE1:
1203 case KEY_VOLUME_ZONE1:
1204 case KEY_MUTE_ZONE1:
1205 case KEY_BASS_ZONE1:
1206 case KEY_TREBLE_ZONE1:
1207 case KEY_BALANCE_ZONE1:
1208 case KEY_FREQ_ZONE1:
1211 case KEY_POWER_ZONE2:
1212 case KEY_SOURCE_ZONE2:
1213 case KEY_INPUT_ZONE2:
1214 case KEY_VOLUME_ZONE2:
1215 case KEY_MUTE_ZONE2:
1216 case KEY_BASS_ZONE2:
1217 case KEY_TREBLE_ZONE2:
1218 case KEY_BALANCE_ZONE2:
1219 case KEY_FREQ_ZONE2:
1222 case KEY_POWER_ZONE3:
1223 case KEY_SOURCE_ZONE3:
1224 case KEY_INPUT_ZONE3:
1225 case KEY_VOLUME_ZONE3:
1226 case KEY_MUTE_ZONE3:
1227 case KEY_BASS_ZONE3:
1228 case KEY_TREBLE_ZONE3:
1229 case KEY_BALANCE_ZONE3:
1230 case KEY_FREQ_ZONE3:
1233 case KEY_POWER_ZONE4:
1234 case KEY_SOURCE_ZONE4:
1235 case KEY_INPUT_ZONE4:
1236 case KEY_VOLUME_ZONE4:
1237 case KEY_MUTE_ZONE4:
1238 case KEY_BASS_ZONE4:
1239 case KEY_TREBLE_ZONE4:
1240 case KEY_BALANCE_ZONE4:
1241 case KEY_FREQ_ZONE4:
1250 logger.debug("Reading feedback message failed");
1251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1252 "@text/offline.comm-error-reading-thread");
1256 frontPanelLine1 = value;
1257 updateChannelState(CHANNEL_LINE1);
1260 frontPanelLine2 = value;
1261 updateChannelState(CHANNEL_LINE2);
1264 currentZone = Integer.parseInt(value);
1266 case KEY_RECORD_SEL:
1267 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1270 if (POWER_ON.equalsIgnoreCase(value)) {
1272 } else if (STANDBY.equalsIgnoreCase(value)) {
1274 if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
1275 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1276 handlePowerOffZone(zone);
1279 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1280 schedulePowerOffJob(false);
1282 throw new RotelException("Invalid value");
1285 case KEY_POWER_ZONE2:
1286 case KEY_POWER_ZONE3:
1287 case KEY_POWER_ZONE4:
1288 if (POWER_ON.equalsIgnoreCase(value)) {
1289 handlePowerOnZone(numZone);
1290 } else if (STANDBY.equalsIgnoreCase(value)) {
1291 handlePowerOffZone(numZone);
1293 throw new RotelException("Invalid value");
1296 case KEY_VOLUME_MIN:
1297 minVolume = Integer.parseInt(value);
1298 if (!model.hasDirectVolumeControl()) {
1299 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1303 case KEY_VOLUME_MAX:
1304 maxVolume = Integer.parseInt(value);
1305 if (!model.hasDirectVolumeControl()) {
1306 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1311 case KEY_VOLUME_ZONE1:
1312 case KEY_VOLUME_ZONE2:
1313 case KEY_VOLUME_ZONE3:
1314 case KEY_VOLUME_ZONE4:
1315 fixedVolumeZones[numZone] = false;
1316 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1317 fixedVolumeZones[numZone] = true;
1318 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1319 volumes[numZone] = minVolume;
1320 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1321 volumes[numZone] = maxVolume;
1323 volumes[numZone] = Integer.parseInt(value);
1326 updateChannelState(CHANNEL_VOLUME);
1327 updateChannelState(CHANNEL_MAIN_VOLUME);
1328 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1330 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1331 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1335 case KEY_MUTE_ZONE1:
1336 case KEY_MUTE_ZONE2:
1337 case KEY_MUTE_ZONE3:
1338 case KEY_MUTE_ZONE4:
1339 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1340 mutes[numZone] = true;
1342 updateChannelState(CHANNEL_MUTE);
1343 updateChannelState(CHANNEL_MAIN_MUTE);
1345 updateGroupChannelState(numZone, CHANNEL_MUTE);
1347 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1348 mutes[numZone] = false;
1350 updateChannelState(CHANNEL_MUTE);
1351 updateChannelState(CHANNEL_MAIN_MUTE);
1353 updateGroupChannelState(numZone, CHANNEL_MUTE);
1356 throw new RotelException("Invalid value");
1360 maxToneLevel = Integer.parseInt(value);
1361 minToneLevel = -maxToneLevel;
1363 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1364 minToneLevel, maxToneLevel);
1367 case KEY_BASS_ZONE1:
1368 case KEY_BASS_ZONE2:
1369 case KEY_BASS_ZONE3:
1370 case KEY_BASS_ZONE4:
1371 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1372 basses[numZone] = minToneLevel;
1373 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1374 basses[numZone] = maxToneLevel;
1376 basses[numZone] = Integer.parseInt(value);
1379 updateChannelState(CHANNEL_BASS);
1380 updateChannelState(CHANNEL_MAIN_BASS);
1382 updateGroupChannelState(numZone, CHANNEL_BASS);
1386 case KEY_TREBLE_ZONE1:
1387 case KEY_TREBLE_ZONE2:
1388 case KEY_TREBLE_ZONE3:
1389 case KEY_TREBLE_ZONE4:
1390 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1391 trebles[numZone] = minToneLevel;
1392 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1393 trebles[numZone] = maxToneLevel;
1395 trebles[numZone] = Integer.parseInt(value);
1398 updateChannelState(CHANNEL_TREBLE);
1399 updateChannelState(CHANNEL_MAIN_TREBLE);
1401 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1405 sources[0] = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1406 updateChannelState(CHANNEL_SOURCE);
1407 updateChannelState(CHANNEL_MAIN_SOURCE);
1410 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1411 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1413 case KEY_SOURCE_ZONE2:
1414 case KEY_SOURCE_ZONE3:
1415 case KEY_SOURCE_ZONE4:
1416 case KEY_INPUT_ZONE1:
1417 case KEY_INPUT_ZONE2:
1418 case KEY_INPUT_ZONE3:
1419 case KEY_INPUT_ZONE4:
1420 sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
1421 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1424 if ("dolby_pliix_movie".equals(value)) {
1425 value = "dolby_plii_movie";
1426 } else if ("dolby_pliix_music".equals(value)) {
1427 value = "dolby_plii_music";
1428 } else if ("dolby_pliix_game".equals(value)) {
1429 value = "dolby_plii_game";
1431 dsp = model.getDspFromFeedback(value);
1432 logger.debug("DSP {}", dsp.getName());
1433 updateChannelState(CHANNEL_DSP);
1434 updateChannelState(CHANNEL_MAIN_DSP);
1436 case KEY1_PLAY_STATUS:
1437 case KEY2_PLAY_STATUS:
1438 if (PLAY.equalsIgnoreCase(value)) {
1439 playStatus = RotelPlayStatus.PLAYING;
1440 updateChannelState(CHANNEL_PLAY_CONTROL);
1441 } else if (PAUSE.equalsIgnoreCase(value)) {
1442 playStatus = RotelPlayStatus.PAUSED;
1443 updateChannelState(CHANNEL_PLAY_CONTROL);
1444 } else if (STOP.equalsIgnoreCase(value)) {
1445 playStatus = RotelPlayStatus.STOPPED;
1446 updateChannelState(CHANNEL_PLAY_CONTROL);
1448 throw new RotelException("Invalid value");
1452 RotelSource source = sources[0];
1453 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1454 track = Integer.parseInt(value);
1455 updateChannelState(CHANNEL_TRACK);
1459 case KEY_FREQ_ZONE1:
1460 case KEY_FREQ_ZONE2:
1461 case KEY_FREQ_ZONE3:
1462 case KEY_FREQ_ZONE4:
1463 if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
1464 frequencies[numZone] = 0.0;
1466 // Suppress a potential ending "k" or "K"
1467 if (value.toUpperCase().endsWith("K")) {
1468 value = value.substring(0, value.length() - 1);
1470 frequencies[numZone] = Double.parseDouble(value);
1473 updateChannelState(CHANNEL_FREQUENCY);
1475 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1479 brightness = Integer.parseInt(value);
1480 updateChannelState(CHANNEL_BRIGHTNESS);
1481 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1483 case KEY_UPDATE_MODE:
1484 case KEY_DISPLAY_UPDATE:
1487 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1489 updateChannelState(CHANNEL_TCBYPASS);
1490 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1492 updateChannelState(CHANNEL_TCBYPASS);
1494 throw new RotelException("Invalid value");
1498 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1500 updateChannelState(CHANNEL_TCBYPASS);
1501 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1503 updateChannelState(CHANNEL_TCBYPASS);
1505 throw new RotelException("Invalid value");
1509 case KEY_BALANCE_ZONE1:
1510 case KEY_BALANCE_ZONE2:
1511 case KEY_BALANCE_ZONE3:
1512 case KEY_BALANCE_ZONE4:
1513 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1514 balances[numZone] = minBalanceLevel;
1515 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1516 balances[numZone] = maxBalanceLevel;
1517 } else if (value.toUpperCase().startsWith("L")) {
1518 balances[numZone] = -Integer.parseInt(value.substring(1));
1519 } else if (value.toUpperCase().startsWith("R")) {
1520 balances[numZone] = Integer.parseInt(value.substring(1));
1522 balances[numZone] = Integer.parseInt(value);
1525 updateChannelState(CHANNEL_BALANCE);
1527 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1531 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1534 updateChannelState(CHANNEL_SPEAKER_A);
1535 updateChannelState(CHANNEL_SPEAKER_B);
1536 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1539 updateChannelState(CHANNEL_SPEAKER_A);
1540 updateChannelState(CHANNEL_SPEAKER_B);
1541 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1544 updateChannelState(CHANNEL_SPEAKER_A);
1545 updateChannelState(CHANNEL_SPEAKER_B);
1546 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1549 updateChannelState(CHANNEL_SPEAKER_A);
1550 updateChannelState(CHANNEL_SPEAKER_B);
1552 throw new RotelException("Invalid value");
1556 getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
1559 getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
1562 logger.debug("onNewMessageEvent: unhandled key {}", key);
1565 } catch (NumberFormatException | RotelException e) {
1566 logger.debug("Invalid value {} for key {}", value, key);
1571 * Handle the received information that device power (main zone) is ON
1573 private void handlePowerOn() {
1574 Boolean prev = powers[0];
1576 updateChannelState(CHANNEL_POWER);
1577 updateChannelState(CHANNEL_MAIN_POWER);
1578 updateChannelState(CHANNEL_ALL_POWER);
1579 if ((prev == null) || !prev) {
1580 schedulePowerOnJob();
1585 * Handle the received information that device power (main zone) is OFF
1587 private void handlePowerOff() {
1588 cancelPowerOnZoneJob(0);
1590 updateChannelState(CHANNEL_POWER);
1591 updateChannelState(CHANNEL_SOURCE);
1592 updateChannelState(CHANNEL_DSP);
1593 updateChannelState(CHANNEL_VOLUME);
1594 updateChannelState(CHANNEL_MUTE);
1595 updateChannelState(CHANNEL_BASS);
1596 updateChannelState(CHANNEL_TREBLE);
1597 updateChannelState(CHANNEL_PLAY_CONTROL);
1598 updateChannelState(CHANNEL_TRACK);
1599 updateChannelState(CHANNEL_FREQUENCY);
1600 updateChannelState(CHANNEL_BRIGHTNESS);
1601 updateChannelState(CHANNEL_TCBYPASS);
1602 updateChannelState(CHANNEL_BALANCE);
1603 updateChannelState(CHANNEL_SPEAKER_A);
1604 updateChannelState(CHANNEL_SPEAKER_B);
1606 updateChannelState(CHANNEL_MAIN_POWER);
1607 updateChannelState(CHANNEL_MAIN_SOURCE);
1608 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1609 updateChannelState(CHANNEL_MAIN_DSP);
1610 updateChannelState(CHANNEL_MAIN_VOLUME);
1611 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1612 updateChannelState(CHANNEL_MAIN_MUTE);
1613 updateChannelState(CHANNEL_MAIN_BASS);
1614 updateChannelState(CHANNEL_MAIN_TREBLE);
1616 updateChannelState(CHANNEL_ALL_POWER);
1617 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1621 * Handle the received information that a zone power is ON
1623 private void handlePowerOnZone(int numZone) {
1624 Boolean prev = powers[numZone];
1625 powers[numZone] = true;
1626 updateGroupChannelState(numZone, CHANNEL_POWER);
1627 if ((prev == null) || !prev) {
1628 schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
1633 * Handle the received information that a zone power is OFF
1635 private void handlePowerOffZone(int numZone) {
1636 cancelPowerOnZoneJob(numZone);
1637 powers[numZone] = false;
1638 updateGroupChannelState(numZone, CHANNEL_POWER);
1639 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1640 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1641 updateGroupChannelState(numZone, CHANNEL_MUTE);
1642 updateGroupChannelState(numZone, CHANNEL_BASS);
1643 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1644 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1645 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1646 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1650 * Schedule the job that will consider the device as OFF if no new event is received before its running
1652 * @param switchOffAllZones true if all zones have to be considered as OFF
1654 private void schedulePowerOffJob(boolean switchOffAllZones) {
1655 logger.debug("Schedule power OFF job");
1656 cancelPowerOffJob();
1657 powerOffJob = scheduler.schedule(() -> {
1658 logger.debug("Power OFF job");
1660 if (switchOffAllZones) {
1661 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1662 handlePowerOffZone(zone);
1665 }, 2000, TimeUnit.MILLISECONDS);
1669 * Cancel the job that will consider the device as OFF
1671 private void cancelPowerOffJob() {
1672 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1673 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1674 powerOffJob.cancel(true);
1675 this.powerOffJob = null;
1680 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1682 private void schedulePowerOnJob() {
1683 logger.debug("Schedule power ON job");
1684 cancelPowerOnZoneJob(0);
1685 powerOnZoneJobs[0] = scheduler.schedule(() -> {
1686 synchronized (sequenceLock) {
1687 logger.debug("Power ON job");
1691 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1692 sendCommand(getVolumeDownCommand(0));
1694 sendCommand(getVolumeUpCommand(0));
1697 if (model.getNumberOfZones() > 1) {
1698 if (currentZone != 1
1699 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1700 selectZone(1, model.getZoneSelectCmd());
1701 } else if (!selectingRecord) {
1702 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1706 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1709 if (model.hasToneControl()) {
1710 if (model == RotelModel.RSX1065) {
1711 // No tone control select command
1712 sendCommand(RotelCommand.TREBLE_DOWN);
1714 sendCommand(RotelCommand.TREBLE_UP);
1716 sendCommand(RotelCommand.BASS_DOWN);
1718 sendCommand(RotelCommand.BASS_UP);
1721 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1726 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1727 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1728 sendCommand(RotelCommand.UPDATE_AUTO);
1729 Thread.sleep(SLEEP_INTV);
1731 if (model.hasSourceControl()) {
1732 sendCommand(RotelCommand.SOURCE);
1733 Thread.sleep(SLEEP_INTV);
1735 if (model.hasVolumeControl() || model.hasToneControl()) {
1736 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1737 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1738 sendCommand(RotelCommand.VOLUME_GET_MIN);
1739 Thread.sleep(SLEEP_INTV);
1740 sendCommand(RotelCommand.VOLUME_GET_MAX);
1741 Thread.sleep(SLEEP_INTV);
1743 if (model.hasToneControl()) {
1744 sendCommand(RotelCommand.TONE_MAX);
1745 Thread.sleep(SLEEP_INTV);
1747 // Wait enough to be sure to get the min/max values requested just before
1749 if (model.hasVolumeControl()) {
1750 sendCommand(RotelCommand.VOLUME_GET);
1751 Thread.sleep(SLEEP_INTV);
1752 if (model != RotelModel.RA11 && model != RotelModel.RA12
1753 && model != RotelModel.RCX1500) {
1754 sendCommand(RotelCommand.MUTE);
1755 Thread.sleep(SLEEP_INTV);
1758 if (model.hasToneControl()) {
1759 sendCommand(RotelCommand.BASS);
1760 Thread.sleep(SLEEP_INTV);
1761 sendCommand(RotelCommand.TREBLE);
1762 Thread.sleep(SLEEP_INTV);
1763 if (model.canGetBypassStatus()) {
1764 sendCommand(RotelCommand.TONE_CONTROLS);
1765 Thread.sleep(SLEEP_INTV);
1769 if (model.hasBalanceControl()) {
1770 sendCommand(RotelCommand.BALANCE);
1771 Thread.sleep(SLEEP_INTV);
1773 if (model.hasPlayControl()) {
1774 RotelSource source = sources[0];
1775 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1776 && (model != RotelModel.RCX1500 || source == null
1777 || !source.getName().equals("CD"))) {
1778 sendCommand(RotelCommand.PLAY_STATUS);
1779 Thread.sleep(SLEEP_INTV);
1781 sendCommand(RotelCommand.CD_PLAY_STATUS);
1782 Thread.sleep(SLEEP_INTV);
1785 if (model.hasDspControl()) {
1786 sendCommand(RotelCommand.DSP_MODE);
1787 Thread.sleep(SLEEP_INTV);
1789 if (model.canGetFrequency()) {
1790 sendCommand(RotelCommand.FREQUENCY);
1791 Thread.sleep(SLEEP_INTV);
1793 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1794 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1795 Thread.sleep(SLEEP_INTV);
1797 if (model.hasSpeakerGroups()) {
1798 sendCommand(RotelCommand.SPEAKER);
1799 Thread.sleep(SLEEP_INTV);
1803 sendCommand(RotelCommand.UPDATE_AUTO);
1804 Thread.sleep(SLEEP_INTV);
1805 if (model.hasSourceControl()) {
1806 if (model.getNumberOfZones() > 1) {
1807 sendCommand(RotelCommand.INPUT);
1809 sendCommand(RotelCommand.SOURCE);
1811 Thread.sleep(SLEEP_INTV);
1813 if (model.hasVolumeControl()) {
1814 sendCommand(RotelCommand.VOLUME_GET);
1815 Thread.sleep(SLEEP_INTV);
1816 sendCommand(RotelCommand.MUTE);
1817 Thread.sleep(SLEEP_INTV);
1819 if (model.hasToneControl()) {
1820 sendCommand(RotelCommand.BASS);
1821 Thread.sleep(SLEEP_INTV);
1822 sendCommand(RotelCommand.TREBLE);
1823 Thread.sleep(SLEEP_INTV);
1824 if (model.canGetBypassStatus()) {
1825 sendCommand(RotelCommand.TCBYPASS);
1826 Thread.sleep(SLEEP_INTV);
1829 if (model.hasBalanceControl()) {
1830 sendCommand(RotelCommand.BALANCE);
1831 Thread.sleep(SLEEP_INTV);
1833 if (model.hasPlayControl()) {
1834 sendCommand(RotelCommand.PLAY_STATUS);
1835 Thread.sleep(SLEEP_INTV);
1836 RotelSource source = sources[0];
1837 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1838 sendCommand(RotelCommand.TRACK);
1839 Thread.sleep(SLEEP_INTV);
1842 if (model.hasDspControl()) {
1843 sendCommand(RotelCommand.DSP_MODE);
1844 Thread.sleep(SLEEP_INTV);
1846 if (model.canGetFrequency()) {
1847 sendCommand(RotelCommand.FREQUENCY);
1848 Thread.sleep(SLEEP_INTV);
1850 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1851 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1852 Thread.sleep(SLEEP_INTV);
1854 if (model.hasSpeakerGroups()) {
1855 sendCommand(RotelCommand.SPEAKER);
1856 Thread.sleep(SLEEP_INTV);
1858 sendCommand(RotelCommand.MODEL);
1859 Thread.sleep(SLEEP_INTV);
1860 sendCommand(RotelCommand.VERSION);
1861 Thread.sleep(SLEEP_INTV);
1864 } catch (RotelException e) {
1865 logger.debug("Init sequence failed: {}", e.getMessage());
1866 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1867 "@text/offline.comm-error-init-sequence");
1869 } catch (InterruptedException e) {
1870 logger.debug("Init sequence interrupted: {}", e.getMessage());
1871 Thread.currentThread().interrupt();
1874 }, 2500, TimeUnit.MILLISECONDS);
1878 * Schedule the job to run with a few seconds delay when the zone power switched ON
1880 private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
1881 logger.debug("Schedule power ON zone {} job", numZone);
1882 cancelPowerOnZoneJob(numZone);
1883 powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
1884 synchronized (sequenceLock) {
1885 logger.debug("Power ON zone {} job", numZone);
1887 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
1888 selectZone(numZone, model.getZoneSelectCmd());
1889 sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
1891 sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
1894 } catch (RotelException e) {
1895 logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
1896 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1897 String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
1899 } catch (InterruptedException e) {
1900 logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
1901 Thread.currentThread().interrupt();
1904 }, 2500, TimeUnit.MILLISECONDS);
1908 * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
1910 private void cancelPowerOnZoneJob(int numZone) {
1911 ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
1912 if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
1913 powerOnZoneJob.cancel(true);
1914 powerOnZoneJobs[numZone] = null;
1919 * Schedule the reconnection job
1921 private void scheduleReconnectJob() {
1922 logger.debug("Schedule reconnect job");
1923 cancelReconnectJob();
1924 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
1925 if (!connector.isConnected()) {
1926 logger.debug("Trying to reconnect...");
1929 String error = null;
1930 if (openConnection()) {
1931 synchronized (sequenceLock) {
1932 schedulePowerOffJob(true);
1934 sendCommand(model.getPowerStateCmd());
1935 } catch (RotelException e) {
1936 error = "@text/offline.comm-error-first-command-after-reconnection";
1937 logger.debug("First command after connection failed", e);
1938 cancelPowerOffJob();
1943 error = "@text/offline.comm-error-reconnection";
1945 if (error != null) {
1947 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1948 handlePowerOffZone(zone);
1950 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
1952 updateStatus(ThingStatus.ONLINE);
1955 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
1959 * Cancel the reconnection job
1961 private void cancelReconnectJob() {
1962 ScheduledFuture<?> reconnectJob = this.reconnectJob;
1963 if (reconnectJob != null && !reconnectJob.isCancelled()) {
1964 reconnectJob.cancel(true);
1965 this.reconnectJob = null;
1969 private void updateGroupChannelState(int numZone, String channel) {
1970 updateChannelState(String.format("zone%d#%s", numZone, channel));
1974 * Update the state of a channel
1976 * @param channel the channel
1978 private void updateChannelState(String channel) {
1979 if (!isLinked(channel)) {
1982 State state = UnDefType.UNDEF;
1983 RotelSource localSource;
1986 case CHANNEL_ZONE1_SOURCE:
1987 case CHANNEL_ZONE1_VOLUME:
1988 case CHANNEL_ZONE1_MUTE:
1989 case CHANNEL_ZONE1_BASS:
1990 case CHANNEL_ZONE1_TREBLE:
1991 case CHANNEL_ZONE1_BALANCE:
1992 case CHANNEL_ZONE1_FREQUENCY:
1995 case CHANNEL_ZONE2_POWER:
1996 case CHANNEL_ZONE2_SOURCE:
1997 case CHANNEL_ZONE2_VOLUME:
1998 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
1999 case CHANNEL_ZONE2_MUTE:
2000 case CHANNEL_ZONE2_BASS:
2001 case CHANNEL_ZONE2_TREBLE:
2002 case CHANNEL_ZONE2_BALANCE:
2003 case CHANNEL_ZONE2_FREQUENCY:
2006 case CHANNEL_ZONE3_POWER:
2007 case CHANNEL_ZONE3_SOURCE:
2008 case CHANNEL_ZONE3_VOLUME:
2009 case CHANNEL_ZONE3_MUTE:
2010 case CHANNEL_ZONE3_BASS:
2011 case CHANNEL_ZONE3_TREBLE:
2012 case CHANNEL_ZONE3_BALANCE:
2013 case CHANNEL_ZONE3_FREQUENCY:
2016 case CHANNEL_ZONE4_POWER:
2017 case CHANNEL_ZONE4_SOURCE:
2018 case CHANNEL_ZONE4_VOLUME:
2019 case CHANNEL_ZONE4_MUTE:
2020 case CHANNEL_ZONE4_BASS:
2021 case CHANNEL_ZONE4_TREBLE:
2022 case CHANNEL_ZONE4_BALANCE:
2023 case CHANNEL_ZONE4_FREQUENCY:
2031 case CHANNEL_MAIN_POWER:
2032 case CHANNEL_ALL_POWER:
2033 case CHANNEL_ZONE2_POWER:
2034 case CHANNEL_ZONE3_POWER:
2035 case CHANNEL_ZONE4_POWER:
2036 Boolean powerZone = powers[numZone];
2037 if (powerZone != null) {
2038 state = OnOffType.from(powerZone.booleanValue());
2041 case CHANNEL_SOURCE:
2042 case CHANNEL_MAIN_SOURCE:
2043 case CHANNEL_ZONE1_SOURCE:
2044 case CHANNEL_ZONE2_SOURCE:
2045 case CHANNEL_ZONE3_SOURCE:
2046 case CHANNEL_ZONE4_SOURCE:
2047 localSource = sources[numZone];
2048 if (isPowerOn(numZone) && localSource != null) {
2049 state = new StringType(localSource.getName());
2052 case CHANNEL_MAIN_RECORD_SOURCE:
2053 localSource = recordSource;
2054 if (isPowerOn() && localSource != null) {
2055 state = new StringType(localSource.getName());
2059 case CHANNEL_MAIN_DSP:
2061 state = new StringType(dsp.getName());
2064 case CHANNEL_VOLUME:
2065 case CHANNEL_MAIN_VOLUME:
2066 case CHANNEL_ZONE1_VOLUME:
2067 case CHANNEL_ZONE2_VOLUME:
2068 case CHANNEL_ZONE3_VOLUME:
2069 case CHANNEL_ZONE4_VOLUME:
2070 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2071 long volumePct = Math
2072 .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2073 state = new PercentType(BigDecimal.valueOf(volumePct));
2076 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2077 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2078 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2079 state = new DecimalType(volumes[numZone]);
2083 case CHANNEL_MAIN_MUTE:
2084 case CHANNEL_ZONE1_MUTE:
2085 case CHANNEL_ZONE2_MUTE:
2086 case CHANNEL_ZONE3_MUTE:
2087 case CHANNEL_ZONE4_MUTE:
2088 if (isPowerOn(numZone)) {
2089 state = OnOffType.from(mutes[numZone]);
2093 case CHANNEL_MAIN_BASS:
2094 case CHANNEL_ZONE1_BASS:
2095 case CHANNEL_ZONE2_BASS:
2096 case CHANNEL_ZONE3_BASS:
2097 case CHANNEL_ZONE4_BASS:
2098 if (isPowerOn(numZone)) {
2099 state = new DecimalType(basses[numZone]);
2102 case CHANNEL_TREBLE:
2103 case CHANNEL_MAIN_TREBLE:
2104 case CHANNEL_ZONE1_TREBLE:
2105 case CHANNEL_ZONE2_TREBLE:
2106 case CHANNEL_ZONE3_TREBLE:
2107 case CHANNEL_ZONE4_TREBLE:
2108 if (isPowerOn(numZone)) {
2109 state = new DecimalType(trebles[numZone]);
2113 if (isPowerOn() && track > 0) {
2114 state = new DecimalType(track);
2117 case CHANNEL_PLAY_CONTROL:
2119 switch (playStatus) {
2121 state = PlayPauseType.PLAY;
2125 state = PlayPauseType.PAUSE;
2130 case CHANNEL_FREQUENCY:
2131 case CHANNEL_ZONE1_FREQUENCY:
2132 case CHANNEL_ZONE2_FREQUENCY:
2133 case CHANNEL_ZONE3_FREQUENCY:
2134 case CHANNEL_ZONE4_FREQUENCY:
2135 if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
2136 state = new DecimalType(frequencies[numZone]);
2140 state = new StringType(frontPanelLine1);
2143 state = new StringType(frontPanelLine2);
2145 case CHANNEL_BRIGHTNESS:
2146 case CHANNEL_ALL_BRIGHTNESS:
2147 if (isPowerOn() && model.hasDimmerControl()) {
2148 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2149 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2150 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2153 case CHANNEL_TCBYPASS:
2155 state = OnOffType.from(tcbypass);
2158 case CHANNEL_BALANCE:
2159 case CHANNEL_ZONE1_BALANCE:
2160 case CHANNEL_ZONE2_BALANCE:
2161 case CHANNEL_ZONE3_BALANCE:
2162 case CHANNEL_ZONE4_BALANCE:
2163 if (isPowerOn(numZone)) {
2164 state = new DecimalType(balances[numZone]);
2167 case CHANNEL_SPEAKER_A:
2169 state = OnOffType.from(speakera);
2172 case CHANNEL_SPEAKER_B:
2174 state = OnOffType.from(speakerb);
2180 updateState(channel, state);
2184 * Inform about the device / main zone power state
2186 * @return true if device / main zone power state is known and known as ON
2188 private boolean isPowerOn() {
2189 return isPowerOn(0);
2193 * Inform about the power state
2195 * @param numZone the zone number (1-4) or 0 for the device or main zone
2197 * @return true if power state is known and known as ON
2199 private boolean isPowerOn(int numZone) {
2200 if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
2201 throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
2203 Boolean power = powers[numZone];
2204 return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
2208 * Get the command to be used for POWER ON
2210 * @param numZone the zone number (2-4) or 0 for the device or main zone
2212 * @return the command
2214 private RotelCommand getPowerOnCommand(int numZone) {
2217 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2219 return RotelCommand.ZONE2_POWER_ON;
2221 return RotelCommand.ZONE3_POWER_ON;
2223 return RotelCommand.ZONE4_POWER_ON;
2225 throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
2230 * Get the command to be used for POWER OFF
2232 * @param numZone the zone number (2-4) or 0 for the device or main zone
2234 * @return the command
2236 private RotelCommand getPowerOffCommand(int numZone) {
2239 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2241 return RotelCommand.ZONE2_POWER_OFF;
2243 return RotelCommand.ZONE3_POWER_OFF;
2245 return RotelCommand.ZONE4_POWER_OFF;
2247 throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
2252 * Get the command to be used for VOLUME UP
2254 * @param numZone the zone number (1-4) or 0 for the device or main zone
2256 * @return the command
2258 private RotelCommand getVolumeUpCommand(int numZone) {
2261 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2263 return RotelCommand.ZONE1_VOLUME_UP;
2265 return RotelCommand.ZONE2_VOLUME_UP;
2267 return RotelCommand.ZONE3_VOLUME_UP;
2269 return RotelCommand.ZONE4_VOLUME_UP;
2271 throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
2276 * Get the command to be used for VOLUME DOWN
2278 * @param numZone the zone number (1-4) or 0 for the device or main zone
2280 * @return the command
2282 private RotelCommand getVolumeDownCommand(int numZone) {
2285 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2286 : RotelCommand.VOLUME_DOWN;
2288 return RotelCommand.ZONE1_VOLUME_DOWN;
2290 return RotelCommand.ZONE2_VOLUME_DOWN;
2292 return RotelCommand.ZONE3_VOLUME_DOWN;
2294 return RotelCommand.ZONE4_VOLUME_DOWN;
2296 throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
2301 * Get the command to be used for VOLUME SET
2303 * @param numZone the zone number (1-4) or 0 for the device
2305 * @return the command
2307 private RotelCommand getVolumeSetCommand(int numZone) {
2310 return RotelCommand.VOLUME_SET;
2312 return RotelCommand.ZONE1_VOLUME_SET;
2314 return RotelCommand.ZONE2_VOLUME_SET;
2316 return RotelCommand.ZONE3_VOLUME_SET;
2318 return RotelCommand.ZONE4_VOLUME_SET;
2320 throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
2325 * Get the command to be used for MUTE ON
2327 * @param numZone the zone number (1-4) or 0 for the device or main zone
2329 * @return the command
2331 private RotelCommand getMuteOnCommand(int numZone) {
2334 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2336 return RotelCommand.ZONE1_MUTE_ON;
2338 return RotelCommand.ZONE2_MUTE_ON;
2340 return RotelCommand.ZONE3_MUTE_ON;
2342 return RotelCommand.ZONE4_MUTE_ON;
2344 throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
2349 * Get the command to be used for MUTE OFF
2351 * @param numZone the zone number (1-4) or 0 for the device or main zone
2353 * @return the command
2355 private RotelCommand getMuteOffCommand(int numZone) {
2358 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2360 return RotelCommand.ZONE1_MUTE_OFF;
2362 return RotelCommand.ZONE2_MUTE_OFF;
2364 return RotelCommand.ZONE3_MUTE_OFF;
2366 return RotelCommand.ZONE4_MUTE_OFF;
2368 throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
2373 * Get the command to be used for MUTE TOGGLE
2375 * @param numZone the zone number (1-4) or 0 for the device or main zone
2377 * @return the command
2379 private RotelCommand getMuteToggleCommand(int numZone) {
2382 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2383 : RotelCommand.MUTE_TOGGLE;
2385 return RotelCommand.ZONE1_MUTE_TOGGLE;
2387 return RotelCommand.ZONE2_MUTE_TOGGLE;
2389 return RotelCommand.ZONE3_MUTE_TOGGLE;
2391 return RotelCommand.ZONE4_MUTE_TOGGLE;
2393 throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
2398 * Get the command to be used for BASS UP
2400 * @param numZone the zone number (1-4) or 0 for the device
2402 * @return the command
2404 private RotelCommand getBassUpCommand(int numZone) {
2407 return RotelCommand.BASS_UP;
2409 return RotelCommand.ZONE1_BASS_UP;
2411 return RotelCommand.ZONE2_BASS_UP;
2413 return RotelCommand.ZONE3_BASS_UP;
2415 return RotelCommand.ZONE4_BASS_UP;
2417 throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
2422 * Get the command to be used for BASS DOWN
2424 * @param numZone the zone number (1-4) or 0 for the device
2426 * @return the command
2428 private RotelCommand getBassDownCommand(int numZone) {
2431 return RotelCommand.BASS_DOWN;
2433 return RotelCommand.ZONE1_BASS_DOWN;
2435 return RotelCommand.ZONE2_BASS_DOWN;
2437 return RotelCommand.ZONE3_BASS_DOWN;
2439 return RotelCommand.ZONE4_BASS_DOWN;
2441 throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
2446 * Get the command to be used for BASS SET
2448 * @param numZone the zone number (1-4) or 0 for the device
2450 * @return the command
2452 private RotelCommand getBassSetCommand(int numZone) {
2455 return RotelCommand.BASS_SET;
2457 return RotelCommand.ZONE1_BASS_SET;
2459 return RotelCommand.ZONE2_BASS_SET;
2461 return RotelCommand.ZONE3_BASS_SET;
2463 return RotelCommand.ZONE4_BASS_SET;
2465 throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
2470 * Get the command to be used for TREBLE UP
2472 * @param numZone the zone number (1-4) or 0 for the device
2474 * @return the command
2476 private RotelCommand getTrebleUpCommand(int numZone) {
2479 return RotelCommand.TREBLE_UP;
2481 return RotelCommand.ZONE1_TREBLE_UP;
2483 return RotelCommand.ZONE2_TREBLE_UP;
2485 return RotelCommand.ZONE3_TREBLE_UP;
2487 return RotelCommand.ZONE4_TREBLE_UP;
2489 throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
2494 * Get the command to be used for TREBLE DOWN
2496 * @param numZone the zone number (1-4) or 0 for the device
2498 * @return the command
2500 private RotelCommand getTrebleDownCommand(int numZone) {
2503 return RotelCommand.TREBLE_DOWN;
2505 return RotelCommand.ZONE1_TREBLE_DOWN;
2507 return RotelCommand.ZONE2_TREBLE_DOWN;
2509 return RotelCommand.ZONE3_TREBLE_DOWN;
2511 return RotelCommand.ZONE4_TREBLE_DOWN;
2513 throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
2518 * Get the command to be used for TREBLE SET
2520 * @param numZone the zone number (1-4) or 0 for the device
2522 * @return the command
2524 private RotelCommand getTrebleSetCommand(int numZone) {
2527 return RotelCommand.TREBLE_SET;
2529 return RotelCommand.ZONE1_TREBLE_SET;
2531 return RotelCommand.ZONE2_TREBLE_SET;
2533 return RotelCommand.ZONE3_TREBLE_SET;
2535 return RotelCommand.ZONE4_TREBLE_SET;
2537 throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
2542 * Get the command to be used for BALANCE LEFT
2544 * @param numZone the zone number (1-4) or 0 for the device
2546 * @return the command
2548 private RotelCommand getBalanceLeftCommand(int numZone) {
2551 return RotelCommand.BALANCE_LEFT;
2553 return RotelCommand.ZONE1_BALANCE_LEFT;
2555 return RotelCommand.ZONE2_BALANCE_LEFT;
2557 return RotelCommand.ZONE3_BALANCE_LEFT;
2559 return RotelCommand.ZONE4_BALANCE_LEFT;
2561 throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
2566 * Get the command to be used for BALANCE RIGHT
2568 * @param numZone the zone number (1-4) or 0 for the device
2570 * @return the command
2572 private RotelCommand getBalanceRightCommand(int numZone) {
2575 return RotelCommand.BALANCE_RIGHT;
2577 return RotelCommand.ZONE1_BALANCE_RIGHT;
2579 return RotelCommand.ZONE2_BALANCE_RIGHT;
2581 return RotelCommand.ZONE3_BALANCE_RIGHT;
2583 return RotelCommand.ZONE4_BALANCE_RIGHT;
2585 throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
2590 * Get the command to be used for BALANCE SET
2592 * @param numZone the zone number (1-4) or 0 for the device
2594 * @return the command
2596 private RotelCommand getBalanceSetCommand(int numZone) {
2599 return RotelCommand.BALANCE_SET;
2601 return RotelCommand.ZONE1_BALANCE_SET;
2603 return RotelCommand.ZONE2_BALANCE_SET;
2605 return RotelCommand.ZONE3_BALANCE_SET;
2607 return RotelCommand.ZONE4_BALANCE_SET;
2609 throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
2613 private void sendCommand(RotelCommand cmd) throws RotelException {
2614 sendCommand(cmd, null);
2618 * Request the Rotel device to execute a command
2620 * @param cmd the command to execute
2621 * @param value the integer value to consider for volume, bass or treble adjustment
2623 * @throws RotelException - In case of any problem
2625 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2628 message = protocolHandler.buildCommandMessage(cmd, value);
2629 } catch (RotelException e) {
2630 // Command not supported
2631 logger.debug("sendCommand: {}", e.getMessage());
2634 connector.writeOutput(cmd.getName(), message);
2636 if (connector instanceof RotelSimuConnector) {
2637 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2638 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2639 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2640 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);