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.RotelMessageEvent;
38 import org.openhab.binding.rotel.internal.communication.RotelMessageEventListener;
39 import org.openhab.binding.rotel.internal.communication.RotelProtocol;
40 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
41 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
42 import org.openhab.binding.rotel.internal.communication.RotelSource;
43 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
44 import org.openhab.core.io.transport.serial.SerialPortManager;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.IncreaseDecreaseType;
47 import org.openhab.core.library.types.NextPreviousType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.library.types.PlayPauseType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.openhab.core.types.StateOption;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
66 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
68 * @author Laurent Garnier - Initial contribution
71 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
73 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
75 private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
76 private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
77 private static final boolean USE_SIMULATED_DEVICE = false;
78 private static final int SLEEP_INTV = 30;
80 private @Nullable ScheduledFuture<?> reconnectJob;
81 private @Nullable ScheduledFuture<?> powerOnJob;
82 private @Nullable ScheduledFuture<?> powerOffJob;
83 private @Nullable ScheduledFuture<?> powerOnZone2Job;
84 private @Nullable ScheduledFuture<?> powerOnZone3Job;
85 private @Nullable ScheduledFuture<?> powerOnZone4Job;
87 private RotelStateDescriptionOptionProvider stateDescriptionProvider;
88 private SerialPortManager serialPortManager;
90 private RotelConnector connector = new RotelSimuConnector(DEFAULT_MODEL, RotelProtocol.HEX, new HashMap<>(),
93 private int minVolume;
94 private int maxVolume;
95 private int minToneLevel;
96 private int maxToneLevel;
98 private int currentZone = 1;
99 private boolean selectingRecord;
100 private @Nullable Boolean power;
101 private boolean powerZone2;
102 private boolean powerZone3;
103 private boolean powerZone4;
104 private RotelSource source = RotelSource.CAT0_CD;
105 private @Nullable RotelSource recordSource;
106 private @Nullable RotelSource sourceZone2;
107 private @Nullable RotelSource sourceZone3;
108 private @Nullable RotelSource sourceZone4;
109 private RotelDsp dsp = RotelDsp.CAT1_NONE;
111 private boolean mute;
112 private boolean fixedVolumeZone2;
113 private int volumeZone2;
114 private boolean muteZone2;
115 private boolean fixedVolumeZone3;
116 private int volumeZone3;
117 private boolean muteZone3;
118 private boolean fixedVolumeZone4;
119 private int volumeZone4;
120 private boolean muteZone4;
123 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
125 private double frequency;
126 private String frontPanelLine1 = "";
127 private String frontPanelLine2 = "";
128 private int brightness;
129 private boolean tcbypass;
131 private int minBalanceLevel;
132 private int maxBalanceLevel;
133 private boolean speakera;
134 private boolean speakerb;
136 private Object sequenceLock = new Object();
141 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
142 SerialPortManager serialPortManager) {
144 this.stateDescriptionProvider = stateDescriptionProvider;
145 this.serialPortManager = serialPortManager;
149 public void initialize() {
150 logger.debug("Start initializing handler for thing {}", getThing().getUID());
152 RotelModel rotelModel;
153 switch (getThing().getThingTypeUID().getId()) {
154 case THING_TYPE_ID_RSP1066:
155 rotelModel = RotelModel.RSP1066;
157 case THING_TYPE_ID_RSP1068:
158 rotelModel = RotelModel.RSP1068;
160 case THING_TYPE_ID_RSP1069:
161 rotelModel = RotelModel.RSP1069;
163 case THING_TYPE_ID_RSP1098:
164 rotelModel = RotelModel.RSP1098;
166 case THING_TYPE_ID_RSP1570:
167 rotelModel = RotelModel.RSP1570;
169 case THING_TYPE_ID_RSP1572:
170 rotelModel = RotelModel.RSP1572;
172 case THING_TYPE_ID_RSX1055:
173 rotelModel = RotelModel.RSX1055;
175 case THING_TYPE_ID_RSX1056:
176 rotelModel = RotelModel.RSX1056;
178 case THING_TYPE_ID_RSX1057:
179 rotelModel = RotelModel.RSX1057;
181 case THING_TYPE_ID_RSX1058:
182 rotelModel = RotelModel.RSX1058;
184 case THING_TYPE_ID_RSX1065:
185 rotelModel = RotelModel.RSX1065;
187 case THING_TYPE_ID_RSX1067:
188 rotelModel = RotelModel.RSX1067;
190 case THING_TYPE_ID_RSX1550:
191 rotelModel = RotelModel.RSX1550;
193 case THING_TYPE_ID_RSX1560:
194 rotelModel = RotelModel.RSX1560;
196 case THING_TYPE_ID_RSX1562:
197 rotelModel = RotelModel.RSX1562;
199 case THING_TYPE_ID_A11:
200 rotelModel = RotelModel.A11;
202 case THING_TYPE_ID_A12:
203 rotelModel = RotelModel.A12;
205 case THING_TYPE_ID_A14:
206 rotelModel = RotelModel.A14;
208 case THING_TYPE_ID_CD11:
209 rotelModel = RotelModel.CD11;
211 case THING_TYPE_ID_CD14:
212 rotelModel = RotelModel.CD14;
214 case THING_TYPE_ID_RA11:
215 rotelModel = RotelModel.RA11;
217 case THING_TYPE_ID_RA12:
218 rotelModel = RotelModel.RA12;
220 case THING_TYPE_ID_RA1570:
221 rotelModel = RotelModel.RA1570;
223 case THING_TYPE_ID_RA1572:
224 rotelModel = RotelModel.RA1572;
226 case THING_TYPE_ID_RA1592:
227 rotelModel = RotelModel.RA1592;
229 case THING_TYPE_ID_RAP1580:
230 rotelModel = RotelModel.RAP1580;
232 case THING_TYPE_ID_RC1570:
233 rotelModel = RotelModel.RC1570;
235 case THING_TYPE_ID_RC1572:
236 rotelModel = RotelModel.RC1572;
238 case THING_TYPE_ID_RC1590:
239 rotelModel = RotelModel.RC1590;
241 case THING_TYPE_ID_RCD1570:
242 rotelModel = RotelModel.RCD1570;
244 case THING_TYPE_ID_RCD1572:
245 rotelModel = RotelModel.RCD1572;
247 case THING_TYPE_ID_RCX1500:
248 rotelModel = RotelModel.RCX1500;
250 case THING_TYPE_ID_RDD1580:
251 rotelModel = RotelModel.RDD1580;
253 case THING_TYPE_ID_RDG1520:
254 case THING_TYPE_ID_RT09:
255 rotelModel = RotelModel.RDG1520;
257 case THING_TYPE_ID_RSP1576:
258 rotelModel = RotelModel.RSP1576;
260 case THING_TYPE_ID_RSP1582:
261 rotelModel = RotelModel.RSP1582;
263 case THING_TYPE_ID_RT11:
264 rotelModel = RotelModel.RT11;
266 case THING_TYPE_ID_RT1570:
267 rotelModel = RotelModel.RT1570;
269 case THING_TYPE_ID_T11:
270 rotelModel = RotelModel.T11;
272 case THING_TYPE_ID_T14:
273 rotelModel = RotelModel.T14;
275 case THING_TYPE_ID_P5:
276 rotelModel = RotelModel.P5;
278 case THING_TYPE_ID_X3:
279 rotelModel = RotelModel.X3;
281 case THING_TYPE_ID_X5:
282 rotelModel = RotelModel.X5;
285 rotelModel = DEFAULT_MODEL;
289 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
291 RotelProtocol rotelProtocol = RotelProtocol.HEX;
292 if (config.protocol != null && !config.protocol.isEmpty()) {
294 rotelProtocol = RotelProtocol.getFromName(config.protocol);
295 } catch (RotelException e) {
298 Map<String, String> properties = editProperties();
299 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
300 if (property != null && !property.isEmpty()) {
302 rotelProtocol = RotelProtocol.getFromName(property);
303 } catch (RotelException e) {
307 logger.debug("rotelProtocol {}", rotelProtocol.getName());
309 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
310 Map<RotelSource, String> sourcesLabels = new HashMap<>();
312 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
314 connector = new RotelSimuConnector(rotelModel, rotelProtocol, sourcesLabels, readerThreadName);
316 if (rotelModel.hasVolumeControl()) {
317 maxVolume = rotelModel.getVolumeMax();
318 if (!rotelModel.hasDirectVolumeControl()) {
320 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
321 minVolume, maxVolume);
324 if (rotelModel.hasToneControl()) {
325 maxToneLevel = rotelModel.getToneLevelMax();
326 minToneLevel = -maxToneLevel;
328 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
329 minToneLevel, maxToneLevel);
331 if (rotelModel.hasBalanceControl()) {
332 maxBalanceLevel = rotelModel.getBalanceLevelMax();
333 minBalanceLevel = -maxBalanceLevel;
334 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
335 minBalanceLevel, maxBalanceLevel);
338 // Check configuration settings
339 String configError = null;
340 if ((config.serialPort == null || config.serialPort.isEmpty())
341 && (config.host == null || config.host.isEmpty())) {
342 configError = "@text/offline.config-error-unknown-serialport-and-host";
343 } else if (config.host == null || config.host.isEmpty()) {
344 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
345 configError = "@text/offline.config-error-invalid-serial-over-ip";
348 if (config.port == null) {
349 configError = "@text/offline.config-error-unknown-port";
350 } else if (config.port <= 0) {
351 configError = "@text/offline.config-error-invalid-port";
355 if (configError != null) {
356 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
358 for (RotelSource src : rotelModel.getSources()) {
359 // Consider custom input labels
361 switch (src.getName()) {
363 label = config.inputLabelCd;
366 label = config.inputLabelTuner;
369 label = config.inputLabelTape;
372 label = config.inputLabelPhono;
375 label = config.inputLabelVideo1;
378 label = config.inputLabelVideo2;
381 label = config.inputLabelVideo3;
384 label = config.inputLabelVideo4;
387 label = config.inputLabelVideo5;
390 label = config.inputLabelVideo6;
393 label = config.inputLabelUsb;
396 label = config.inputLabelMulti;
401 if (label != null && !label.isEmpty()) {
402 sourcesCustomLabels.put(src, label);
404 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
407 if (USE_SIMULATED_DEVICE) {
408 connector = new RotelSimuConnector(rotelModel, rotelProtocol, sourcesLabels, readerThreadName);
409 } else if (config.serialPort != null) {
410 connector = new RotelSerialConnector(serialPortManager, config.serialPort, rotelModel, rotelProtocol,
411 sourcesLabels, readerThreadName);
413 connector = new RotelIpConnector(config.host, config.port, rotelModel, rotelProtocol, sourcesLabels,
417 if (rotelModel.hasSourceControl()) {
418 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
419 getStateOptions(rotelModel.getSources(), sourcesCustomLabels));
420 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
421 getStateOptions(rotelModel.getSources(), sourcesCustomLabels));
422 stateDescriptionProvider.setStateOptions(
423 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
424 getStateOptions(rotelModel.getRecordSources(), sourcesCustomLabels));
426 if (rotelModel.hasZone2SourceControl()) {
427 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
428 getStateOptions(rotelModel.getZone2Sources(), sourcesCustomLabels));
430 if (rotelModel.hasZone3SourceControl()) {
431 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
432 getStateOptions(rotelModel.getZone3Sources(), sourcesCustomLabels));
434 if (rotelModel.hasZone4SourceControl()) {
435 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
436 getStateOptions(rotelModel.getZone4Sources(), sourcesCustomLabels));
438 if (rotelModel.hasDspControl()) {
439 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
440 rotelModel.getDspStateOptions());
441 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
442 rotelModel.getDspStateOptions());
445 updateStatus(ThingStatus.UNKNOWN);
447 scheduleReconnectJob();
450 logger.debug("Finished initializing!");
454 public void dispose() {
455 logger.debug("Disposing handler for thing {}", getThing().getUID());
458 cancelPowerOnZone2Job();
459 cancelPowerOnZone3Job();
460 cancelPowerOnZone4Job();
461 cancelReconnectJob();
466 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
467 List<StateOption> options = new ArrayList<>();
468 for (RotelSource item : list) {
469 String label = sourcesLabels.get(item);
470 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
476 public void handleCommand(ChannelUID channelUID, Command command) {
477 String channel = channelUID.getId();
479 if (getThing().getStatus() != ThingStatus.ONLINE) {
480 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
484 if (command instanceof RefreshType) {
485 updateChannelState(channel);
489 if (!connector.isConnected()) {
490 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
496 boolean success = true;
497 synchronized (sequenceLock) {
501 case CHANNEL_MAIN_POWER:
502 handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
504 case CHANNEL_ZONE2_POWER:
505 if (connector.getModel().hasZone2Commands()) {
506 handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
507 } else if (connector.getModel().getNbAdditionalZones() == 1) {
508 if (isPowerOn() || powerZone2) {
509 selectZone(2, connector.getModel().getZoneSelectCmd());
511 connector.sendCommand(RotelCommand.ZONE_SELECT);
514 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
517 case CHANNEL_ZONE3_POWER:
518 if (connector.getModel().hasZone3Commands()) {
519 handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
522 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
525 case CHANNEL_ZONE4_POWER:
526 if (connector.getModel().hasZone4Commands()) {
527 handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
530 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
534 case CHANNEL_MAIN_SOURCE:
537 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
539 src = connector.getModel().getSourceFromName(command.toString());
540 cmd = connector.getModel().hasOtherThanPrimaryCommands() ? src.getMainZoneCommand()
543 connector.sendCommand(cmd);
544 if (connector.getModel().canGetFrequency()) {
545 // send <new-source> returns
546 // 1.) the selected <new-source>
547 // 2.) the used frequency
549 // at response-time the frequency has the value of <old-source>
550 // so we must wait a short moment to get the frequency of <new-source>
552 connector.sendCommand(RotelCommand.FREQUENCY);
554 updateChannelState(CHANNEL_FREQUENCY);
558 logger.debug("Command {} from channel {} failed: undefined source command", command,
563 case CHANNEL_MAIN_RECORD_SOURCE:
566 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
567 } else if (connector.getModel().hasOtherThanPrimaryCommands()) {
568 src = connector.getModel().getSourceFromName(command.toString());
569 cmd = src.getRecordCommand();
571 connector.sendCommand(cmd);
574 logger.debug("Command {} from channel {} failed: undefined record source command",
578 src = connector.getModel().getSourceFromName(command.toString());
579 cmd = src.getCommand();
581 connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
583 connector.sendCommand(cmd);
586 logger.debug("Command {} from channel {} failed: undefined source command", command,
591 case CHANNEL_ZONE2_SOURCE:
594 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
595 } else if (connector.getModel().hasZone2Commands()) {
596 src = connector.getModel().getSourceFromName(command.toString());
597 cmd = src.getZone2Command();
599 connector.sendCommand(cmd);
602 logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
605 } else if (connector.getModel().getNbAdditionalZones() >= 1) {
606 src = connector.getModel().getSourceFromName(command.toString());
607 cmd = src.getCommand();
609 selectZone(2, connector.getModel().getZoneSelectCmd());
610 connector.sendCommand(cmd);
613 logger.debug("Command {} from channel {} failed: undefined source command", command,
618 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
621 case CHANNEL_ZONE3_SOURCE:
624 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
625 } else if (connector.getModel().hasZone3Commands()) {
626 src = connector.getModel().getSourceFromName(command.toString());
627 cmd = src.getZone3Command();
629 connector.sendCommand(cmd);
632 logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
637 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
640 case CHANNEL_ZONE4_SOURCE:
643 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
644 } else if (connector.getModel().hasZone4Commands()) {
645 src = connector.getModel().getSourceFromName(command.toString());
646 cmd = src.getZone4Command();
648 connector.sendCommand(cmd);
651 logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
656 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
660 case CHANNEL_MAIN_DSP:
663 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
665 connector.sendCommand(connector.getModel().getCommandFromDspName(command.toString()));
669 case CHANNEL_MAIN_VOLUME:
672 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
673 } else if (connector.getModel().hasVolumeControl()) {
674 handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
675 RotelCommand.VOLUME_SET);
678 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
681 case CHANNEL_MAIN_VOLUME_UP_DOWN:
684 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
685 } else if (connector.getModel().hasVolumeControl()) {
686 handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
690 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
693 case CHANNEL_ZONE2_VOLUME:
696 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
697 } else if (fixedVolumeZone2) {
699 logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
701 } else if (connector.getModel().hasVolumeControl()
702 && connector.getModel().getNbAdditionalZones() >= 1) {
703 if (connector.getModel().hasZone2Commands()) {
704 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
705 RotelCommand.ZONE2_VOLUME_DOWN, RotelCommand.ZONE2_VOLUME_SET);
707 selectZone(2, connector.getModel().getZoneSelectCmd());
708 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
709 RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
713 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
716 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
719 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
720 } else if (fixedVolumeZone2) {
722 logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
724 } else if (connector.getModel().hasVolumeControl()
725 && connector.getModel().getNbAdditionalZones() >= 1) {
726 if (connector.getModel().hasZone2Commands()) {
727 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
728 RotelCommand.ZONE2_VOLUME_DOWN, null);
730 selectZone(2, connector.getModel().getZoneSelectCmd());
731 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
732 RotelCommand.VOLUME_DOWN, null);
736 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
739 case CHANNEL_ZONE3_VOLUME:
742 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
743 } else if (fixedVolumeZone3) {
745 logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
747 } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone3Commands()) {
748 handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
749 RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
752 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
755 case CHANNEL_ZONE4_VOLUME:
758 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
759 } else if (fixedVolumeZone4) {
761 logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
763 } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone4Commands()) {
764 handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
765 RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
768 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
772 case CHANNEL_MAIN_MUTE:
775 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
776 } else if (connector.getModel().hasVolumeControl()) {
777 handleMuteCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
778 getMuteOnCommand(), getMuteOffCommand(), getMuteToggleCommand());
781 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
784 case CHANNEL_ZONE2_MUTE:
787 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
788 } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone2Commands()) {
789 handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
790 RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
793 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
796 case CHANNEL_ZONE3_MUTE:
799 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
800 } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone3Commands()) {
801 handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
802 RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
805 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
808 case CHANNEL_ZONE4_MUTE:
811 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
812 } else if (connector.getModel().hasVolumeControl() && connector.getModel().hasZone4Commands()) {
813 handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
814 RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
817 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
821 case CHANNEL_MAIN_BASS:
824 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
825 } else if (tcbypass) {
826 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
828 updateChannelState(CHANNEL_BASS);
830 handleToneCmd(bass, channel, command, 2, RotelCommand.BASS_UP, RotelCommand.BASS_DOWN,
831 RotelCommand.BASS_SET);
835 case CHANNEL_MAIN_TREBLE:
838 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
839 } else if (tcbypass) {
840 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
842 updateChannelState(CHANNEL_TREBLE);
844 handleToneCmd(treble, channel, command, 1, RotelCommand.TREBLE_UP, RotelCommand.TREBLE_DOWN,
845 RotelCommand.TREBLE_SET);
848 case CHANNEL_PLAY_CONTROL:
851 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
852 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
853 connector.sendCommand(RotelCommand.PLAY);
854 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
855 connector.sendCommand(RotelCommand.PAUSE);
856 if (connector.getProtocol() == RotelProtocol.ASCII_V1
857 && connector.getModel() != RotelModel.RCD1570
858 && connector.getModel() != RotelModel.RCD1572
859 && connector.getModel() != RotelModel.RCX1500) {
860 Thread.sleep(SLEEP_INTV);
861 connector.sendCommand(RotelCommand.PLAY_STATUS);
863 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
864 connector.sendCommand(RotelCommand.TRACK_FORWARD);
865 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
866 connector.sendCommand(RotelCommand.TRACK_BACKWORD);
869 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
872 case CHANNEL_BRIGHTNESS:
875 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
876 } else if (!connector.getModel().hasDimmerControl()) {
878 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
879 } else if (command instanceof PercentType) {
880 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
881 * (connector.getModel().getDimmerLevelMax()
882 - connector.getModel().getDimmerLevelMin()))
883 + connector.getModel().getDimmerLevelMin();
884 connector.sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
887 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
890 case CHANNEL_TCBYPASS:
893 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
894 } else if (!connector.getModel().hasToneControl()
895 || connector.getProtocol() == RotelProtocol.HEX) {
897 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
899 handleTcbypassCmd(channel, command,
900 connector.getProtocol() == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
901 : RotelCommand.TCBYPASS_ON,
902 connector.getProtocol() == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
903 : RotelCommand.TCBYPASS_OFF);
906 case CHANNEL_BALANCE:
909 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
910 } else if (!connector.getModel().hasBalanceControl()
911 || connector.getProtocol() == RotelProtocol.HEX) {
913 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
915 handleBalanceCmd(channel, command, RotelCommand.BALANCE_LEFT, RotelCommand.BALANCE_RIGHT,
916 RotelCommand.BALANCE_SET);
919 case CHANNEL_SPEAKER_A:
922 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
924 handleSpeakerCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
925 RotelCommand.SPEAKER_A_ON, RotelCommand.SPEAKER_A_OFF,
926 RotelCommand.SPEAKER_A_TOGGLE);
929 case CHANNEL_SPEAKER_B:
932 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
934 handleSpeakerCmd(connector.getProtocol() == RotelProtocol.HEX, channel, command,
935 RotelCommand.SPEAKER_B_ON, RotelCommand.SPEAKER_B_OFF,
936 RotelCommand.SPEAKER_B_TOGGLE);
941 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
945 logger.debug("Command {} from channel {} succeeded", command, channel);
947 updateChannelState(channel);
949 } catch (RotelException e) {
950 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
951 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
952 "@text/offline.comm-error-sending-command");
954 scheduleReconnectJob();
955 } catch (InterruptedException e) {
956 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
957 Thread.currentThread().interrupt();
963 * Handle a power ON/OFF command
965 * @param channel the channel
966 * @param command the received channel command (OnOffType)
967 * @param onCmd the command to be sent to the device to power it ON
968 * @param offCmd the command to be sent to the device to power it OFF
970 * @throws RotelException in case of communication error with the device
972 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
973 throws RotelException {
974 if (command instanceof OnOffType && command == OnOffType.ON) {
975 connector.sendCommand(onCmd);
976 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
977 connector.sendCommand(offCmd);
979 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
984 * Handle a volume command
986 * @param current the current volume
987 * @param channel the channel
988 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
989 * @param upCmd the command to be sent to the device to increase the volume
990 * @param downCmd the command to be sent to the device to decrease the volume
991 * @param setCmd the command to be sent to the device to set the volume at a value
993 * @throws RotelException in case of communication error with the device
995 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
996 @Nullable RotelCommand setCmd) throws RotelException {
997 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
998 connector.sendCommand(upCmd);
999 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1000 connector.sendCommand(downCmd);
1001 } else if (command instanceof DecimalType && setCmd == null) {
1002 int value = ((DecimalType) command).intValue();
1003 if (value >= minVolume && value <= maxVolume) {
1004 if (value > current) {
1005 connector.sendCommand(upCmd);
1006 } else if (value < current) {
1007 connector.sendCommand(downCmd);
1010 } else if (command instanceof PercentType && setCmd != null) {
1011 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1013 connector.sendCommand(setCmd, value);
1015 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1020 * Handle a mute command
1022 * @param onlyToggle true if only the toggle command must be used
1023 * @param channel the channel
1024 * @param command the received channel command (OnOffType)
1025 * @param onCmd the command to be sent to the device to mute
1026 * @param offCmd the command to be sent to the device to unmute
1027 * @param toggleCmd the command to be sent to the device to toggle the mute state
1029 * @throws RotelException in case of communication error with the device
1031 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1032 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1033 if (command instanceof OnOffType) {
1035 connector.sendCommand(toggleCmd);
1036 } else if (command == OnOffType.ON) {
1037 connector.sendCommand(onCmd);
1038 } else if (command == OnOffType.OFF) {
1039 connector.sendCommand(offCmd);
1042 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1047 * Handle a tone level adjustment command (bass or treble)
1049 * @param current the current tone level
1050 * @param channel the channel
1051 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1052 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1053 * @param upCmd the command to be sent to the device to increase the tone level
1054 * @param downCmd the command to be sent to the device to decrease the tone level
1055 * @param setCmd the command to be sent to the device to set the tone level at a value
1057 * @throws RotelException in case of communication error with the device
1058 * @throws InterruptedException in case of interruption during a thread sleep
1060 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1061 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1062 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1063 selectToneControl(nbSelect);
1064 connector.sendCommand(upCmd);
1065 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1066 selectToneControl(nbSelect);
1067 connector.sendCommand(downCmd);
1068 } else if (command instanceof DecimalType) {
1069 int value = ((DecimalType) command).intValue();
1070 if (value >= minToneLevel && value <= maxToneLevel) {
1071 if (connector.getProtocol() != RotelProtocol.HEX) {
1072 connector.sendCommand(setCmd, value);
1073 } else if (value > current) {
1074 selectToneControl(nbSelect);
1075 connector.sendCommand(upCmd);
1076 } else if (value < current) {
1077 selectToneControl(nbSelect);
1078 connector.sendCommand(downCmd);
1082 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1087 * Handle a tcbypass command (only for ASCII protocol)
1089 * @param channel the channel
1090 * @param command the received channel command (OnOffType)
1091 * @param onCmd the command to be sent to the device to bypass_on
1092 * @param offCmd the command to be sent to the device to bypass_off
1094 * @throws RotelException in case of communication error with the device
1096 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1097 throws RotelException, InterruptedException {
1098 if (command instanceof OnOffType) {
1099 if (command == OnOffType.ON) {
1100 connector.sendCommand(onCmd);
1103 updateChannelState(CHANNEL_BASS);
1104 updateChannelState(CHANNEL_TREBLE);
1105 } else if (command == OnOffType.OFF) {
1106 connector.sendCommand(offCmd);
1108 connector.sendCommand(RotelCommand.BASS);
1110 connector.sendCommand(RotelCommand.TREBLE);
1113 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1118 * Handle a speaker command
1120 * @param onlyToggle true if only the toggle command must be used
1121 * @param channel the channel
1122 * @param command the received channel command (OnOffType)
1123 * @param onCmd the command to be sent to the device to speaker_x_on
1124 * @param offCmd the command to be sent to the device to speaker_x_off
1125 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1127 * @throws RotelException in case of communication error with the device
1129 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1130 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1131 if (command instanceof OnOffType) {
1133 connector.sendCommand(toggleCmd);
1134 } else if (command == OnOffType.ON) {
1135 connector.sendCommand(onCmd);
1136 } else if (command == OnOffType.OFF) {
1137 connector.sendCommand(offCmd);
1140 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1145 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1147 * @param channel the channel
1148 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1149 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1150 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1151 * @param setCmd the command to be sent to the device to set the balance at a value
1153 * @throws RotelException in case of communication error with the device
1154 * @throws InterruptedException in case of interruption during a thread sleep
1156 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1157 RotelCommand setCmd) throws RotelException, InterruptedException {
1158 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1159 connector.sendCommand(rightCmd);
1160 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1161 connector.sendCommand(leftCmd);
1162 } else if (command instanceof DecimalType) {
1163 int value = ((DecimalType) command).intValue();
1164 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1165 connector.sendCommand(setCmd, value);
1168 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1173 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1175 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1177 * @throws RotelException in case of communication error with the device
1178 * @throws InterruptedException in case of interruption during a thread sleep
1180 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1181 // No tone control select command for RSX-1065
1182 if (connector.getProtocol() == RotelProtocol.HEX && connector.getModel() != RotelModel.RSX1065) {
1183 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1188 * Run a sequence of commands to display a particular zone on the device front panel
1190 * @param zone the zone to be displayed (1 for main zone)
1191 * @param selectCommand the command to be sent to the device to switch the display between zones
1193 * @throws RotelException in case of communication error with the device
1194 * @throws InterruptedException in case of interruption during a thread sleep
1196 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1197 throws RotelException, InterruptedException {
1198 if (connector.getProtocol() == RotelProtocol.HEX && connector.getModel().getNbAdditionalZones() >= 1
1199 && zone >= 1 && zone != currentZone && selectCommand != null) {
1201 if (zone < currentZone) {
1202 nbSelect = zone + connector.getModel().getNbAdditionalZones() - currentZone;
1203 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1207 nbSelect = zone - currentZone;
1208 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1209 && !selectingRecord) {
1213 selectFeature(nbSelect, null, selectCommand);
1218 * Run a sequence of commands to display a particular feature on the device front panel
1220 * @param nbSelect the number of select commands to be run
1221 * @param preCmd the initial command to be sent to the device (before the select commands)
1222 * @param selectCmd the select command to be sent to the device
1224 * @throws RotelException in case of communication error with the device
1225 * @throws InterruptedException in case of interruption during a thread sleep
1227 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1228 throws RotelException, InterruptedException {
1229 if (connector.getProtocol() == RotelProtocol.HEX) {
1230 if (preCmd != null) {
1231 connector.sendCommand(preCmd);
1234 for (int i = 1; i <= nbSelect; i++) {
1235 connector.sendCommand(selectCmd);
1242 * Open the connection with the Rotel device
1244 * @return true if the connection is opened successfully or flase if not
1246 private synchronized boolean openConnection() {
1247 connector.addEventListener(this);
1250 } catch (RotelException e) {
1251 logger.debug("openConnection() failed", e);
1253 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1254 return connector.isConnected();
1258 * Close the connection with the Rotel device
1260 private synchronized void closeConnection() {
1262 connector.removeEventListener(this);
1263 logger.debug("closeConnection(): disconnected");
1267 public void onNewMessageEvent(EventObject event) {
1268 cancelPowerOffJob();
1270 RotelMessageEvent evt = (RotelMessageEvent) event;
1271 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1273 String key = evt.getKey();
1274 String value = evt.getValue().trim();
1275 if (!RotelConnector.KEY_ERROR.equals(key)) {
1276 updateStatus(ThingStatus.ONLINE);
1280 case RotelConnector.KEY_ERROR:
1281 logger.debug("Reading feedback message failed");
1282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1283 "@text/offline.comm-error-reading-thread");
1286 case RotelConnector.KEY_LINE1:
1287 frontPanelLine1 = value;
1288 updateChannelState(CHANNEL_LINE1);
1290 case RotelConnector.KEY_LINE2:
1291 frontPanelLine2 = value;
1292 updateChannelState(CHANNEL_LINE2);
1294 case RotelConnector.KEY_ZONE:
1295 currentZone = Integer.parseInt(value);
1297 case RotelConnector.KEY_RECORD_SEL:
1298 selectingRecord = RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value);
1300 case RotelConnector.KEY_POWER:
1301 if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1303 } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1305 } else if (RotelConnector.POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1306 schedulePowerOffJob(false);
1308 throw new RotelException("Invalid value");
1311 case RotelConnector.KEY_POWER_ZONE2:
1312 if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1313 handlePowerOnZone2();
1314 } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1315 handlePowerOffZone2();
1317 throw new RotelException("Invalid value");
1320 case RotelConnector.KEY_POWER_ZONE3:
1321 if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1322 handlePowerOnZone3();
1323 } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1324 handlePowerOffZone3();
1326 throw new RotelException("Invalid value");
1329 case RotelConnector.KEY_POWER_ZONE4:
1330 if (RotelConnector.POWER_ON.equalsIgnoreCase(value)) {
1331 handlePowerOnZone4();
1332 } else if (RotelConnector.STANDBY.equalsIgnoreCase(value)) {
1333 handlePowerOffZone4();
1335 throw new RotelException("Invalid value");
1338 case RotelConnector.KEY_VOLUME_MIN:
1339 minVolume = Integer.parseInt(value);
1340 if (!connector.getModel().hasDirectVolumeControl()) {
1341 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1345 case RotelConnector.KEY_VOLUME_MAX:
1346 maxVolume = Integer.parseInt(value);
1347 if (!connector.getModel().hasDirectVolumeControl()) {
1348 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1352 case RotelConnector.KEY_VOLUME:
1353 if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1355 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1358 volume = Integer.parseInt(value);
1360 updateChannelState(CHANNEL_VOLUME);
1361 updateChannelState(CHANNEL_MAIN_VOLUME);
1362 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1364 case RotelConnector.KEY_MUTE:
1365 if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1367 updateChannelState(CHANNEL_MUTE);
1368 updateChannelState(CHANNEL_MAIN_MUTE);
1369 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1371 updateChannelState(CHANNEL_MUTE);
1372 updateChannelState(CHANNEL_MAIN_MUTE);
1374 throw new RotelException("Invalid value");
1377 case RotelConnector.KEY_VOLUME_ZONE2:
1378 fixedVolumeZone2 = false;
1379 if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1380 fixedVolumeZone2 = true;
1381 } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1382 volumeZone2 = minVolume;
1383 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1384 volumeZone2 = maxVolume;
1386 volumeZone2 = Integer.parseInt(value);
1388 updateChannelState(CHANNEL_ZONE2_VOLUME);
1389 updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1391 case RotelConnector.KEY_VOLUME_ZONE3:
1392 fixedVolumeZone3 = false;
1393 if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1394 fixedVolumeZone3 = true;
1395 } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1396 volumeZone3 = minVolume;
1397 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1398 volumeZone3 = maxVolume;
1400 volumeZone3 = Integer.parseInt(value);
1402 updateChannelState(CHANNEL_ZONE3_VOLUME);
1404 case RotelConnector.KEY_VOLUME_ZONE4:
1405 fixedVolumeZone4 = false;
1406 if (RotelConnector.MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1407 fixedVolumeZone4 = true;
1408 } else if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1409 volumeZone4 = minVolume;
1410 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1411 volumeZone4 = maxVolume;
1413 volumeZone4 = Integer.parseInt(value);
1415 updateChannelState(CHANNEL_ZONE4_VOLUME);
1417 case RotelConnector.KEY_MUTE_ZONE2:
1418 if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1420 updateChannelState(CHANNEL_ZONE2_MUTE);
1421 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1423 updateChannelState(CHANNEL_ZONE2_MUTE);
1425 throw new RotelException("Invalid value");
1428 case RotelConnector.KEY_MUTE_ZONE3:
1429 if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1431 updateChannelState(CHANNEL_ZONE3_MUTE);
1432 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1434 updateChannelState(CHANNEL_ZONE3_MUTE);
1436 throw new RotelException("Invalid value");
1439 case RotelConnector.KEY_MUTE_ZONE4:
1440 if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1442 updateChannelState(CHANNEL_ZONE4_MUTE);
1443 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1445 updateChannelState(CHANNEL_ZONE4_MUTE);
1447 throw new RotelException("Invalid value");
1450 case RotelConnector.KEY_TONE_MAX:
1451 maxToneLevel = Integer.parseInt(value);
1452 minToneLevel = -maxToneLevel;
1454 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1455 minToneLevel, maxToneLevel);
1457 case RotelConnector.KEY_BASS:
1458 if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1459 bass = minToneLevel;
1460 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1461 bass = maxToneLevel;
1463 bass = Integer.parseInt(value);
1465 updateChannelState(CHANNEL_BASS);
1466 updateChannelState(CHANNEL_MAIN_BASS);
1468 case RotelConnector.KEY_TREBLE:
1469 if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1470 treble = minToneLevel;
1471 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1472 treble = maxToneLevel;
1474 treble = Integer.parseInt(value);
1476 updateChannelState(CHANNEL_TREBLE);
1477 updateChannelState(CHANNEL_MAIN_TREBLE);
1479 case RotelConnector.KEY_SOURCE:
1480 source = connector.getModel().getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1481 updateChannelState(CHANNEL_SOURCE);
1482 updateChannelState(CHANNEL_MAIN_SOURCE);
1484 case RotelConnector.KEY_RECORD:
1485 recordSource = connector.getModel()
1486 .getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1487 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1489 case RotelConnector.KEY_SOURCE_ZONE2:
1490 sourceZone2 = connector.getModel()
1491 .getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1492 updateChannelState(CHANNEL_ZONE2_SOURCE);
1494 case RotelConnector.KEY_SOURCE_ZONE3:
1495 sourceZone3 = connector.getModel()
1496 .getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1497 updateChannelState(CHANNEL_ZONE3_SOURCE);
1499 case RotelConnector.KEY_SOURCE_ZONE4:
1500 sourceZone4 = connector.getModel()
1501 .getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1502 updateChannelState(CHANNEL_ZONE4_SOURCE);
1504 case RotelConnector.KEY_DSP_MODE:
1505 if ("dolby_pliix_movie".equals(value)) {
1506 value = "dolby_plii_movie";
1507 } else if ("dolby_pliix_music".equals(value)) {
1508 value = "dolby_plii_music";
1509 } else if ("dolby_pliix_game".equals(value)) {
1510 value = "dolby_plii_game";
1512 dsp = connector.getModel().getDspFromFeedback(value);
1513 logger.debug("DSP {}", dsp.getName());
1514 updateChannelState(CHANNEL_DSP);
1515 updateChannelState(CHANNEL_MAIN_DSP);
1517 case RotelConnector.KEY1_PLAY_STATUS:
1518 case RotelConnector.KEY2_PLAY_STATUS:
1519 if (RotelConnector.PLAY.equalsIgnoreCase(value)) {
1520 playStatus = RotelPlayStatus.PLAYING;
1521 updateChannelState(CHANNEL_PLAY_CONTROL);
1522 } else if (RotelConnector.PAUSE.equalsIgnoreCase(value)) {
1523 playStatus = RotelPlayStatus.PAUSED;
1524 updateChannelState(CHANNEL_PLAY_CONTROL);
1525 } else if (RotelConnector.STOP.equalsIgnoreCase(value)) {
1526 playStatus = RotelPlayStatus.STOPPED;
1527 updateChannelState(CHANNEL_PLAY_CONTROL);
1529 throw new RotelException("Invalid value");
1532 case RotelConnector.KEY_TRACK:
1533 if (source.getName().equals("CD") && !connector.getModel().hasSourceControl()) {
1534 track = Integer.parseInt(value);
1535 updateChannelState(CHANNEL_TRACK);
1538 case RotelConnector.KEY_FREQ:
1539 if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1542 // Suppress a potential ending "k" or "K"
1543 if (value.toUpperCase().endsWith("K")) {
1544 value = value.substring(0, value.length() - 1);
1546 frequency = Double.parseDouble(value);
1548 updateChannelState(CHANNEL_FREQUENCY);
1550 case RotelConnector.KEY_DIMMER:
1551 brightness = Integer.parseInt(value);
1552 updateChannelState(CHANNEL_BRIGHTNESS);
1554 case RotelConnector.KEY_UPDATE_MODE:
1555 case RotelConnector.KEY_DISPLAY_UPDATE:
1557 case RotelConnector.KEY_TONE:
1558 if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1560 updateChannelState(CHANNEL_TCBYPASS);
1561 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1563 updateChannelState(CHANNEL_TCBYPASS);
1565 throw new RotelException("Invalid value");
1568 case RotelConnector.KEY_TCBYPASS:
1569 if (RotelConnector.MSG_VALUE_ON.equalsIgnoreCase(value)) {
1571 updateChannelState(CHANNEL_TCBYPASS);
1572 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1574 updateChannelState(CHANNEL_TCBYPASS);
1576 throw new RotelException("Invalid value");
1579 case RotelConnector.KEY_BALANCE:
1580 if (RotelConnector.MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1581 balance = minBalanceLevel;
1582 } else if (RotelConnector.MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1583 balance = maxBalanceLevel;
1584 } else if (value.toUpperCase().startsWith("L")) {
1585 balance = -Integer.parseInt(value.substring(1));
1586 } else if (value.toLowerCase().startsWith("R")) {
1587 balance = Integer.parseInt(value.substring(1));
1589 balance = Integer.parseInt(value);
1591 updateChannelState(CHANNEL_BALANCE);
1593 case RotelConnector.KEY_SPEAKER:
1594 if (RotelConnector.MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1597 updateChannelState(CHANNEL_SPEAKER_A);
1598 updateChannelState(CHANNEL_SPEAKER_B);
1599 } else if (RotelConnector.MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1602 updateChannelState(CHANNEL_SPEAKER_A);
1603 updateChannelState(CHANNEL_SPEAKER_B);
1604 } else if (RotelConnector.MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1607 updateChannelState(CHANNEL_SPEAKER_A);
1608 updateChannelState(CHANNEL_SPEAKER_B);
1609 } else if (RotelConnector.MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1612 updateChannelState(CHANNEL_SPEAKER_A);
1613 updateChannelState(CHANNEL_SPEAKER_B);
1615 throw new RotelException("Invalid value");
1619 logger.debug("onNewMessageEvent: unhandled key {}", key);
1622 } catch (NumberFormatException | RotelException e) {
1623 logger.debug("Invalid value {} for key {}", value, key);
1628 * Handle the received information that device power (main zone) is ON
1630 private void handlePowerOn() {
1631 Boolean prev = power;
1633 updateChannelState(CHANNEL_POWER);
1634 updateChannelState(CHANNEL_MAIN_POWER);
1635 if ((prev == null) || !prev) {
1636 schedulePowerOnJob();
1641 * Handle the received information that device power (main zone) is OFF
1643 private void handlePowerOff() {
1646 updateChannelState(CHANNEL_POWER);
1647 updateChannelState(CHANNEL_MAIN_POWER);
1648 updateChannelState(CHANNEL_SOURCE);
1649 updateChannelState(CHANNEL_MAIN_SOURCE);
1650 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1651 updateChannelState(CHANNEL_DSP);
1652 updateChannelState(CHANNEL_MAIN_DSP);
1653 updateChannelState(CHANNEL_VOLUME);
1654 updateChannelState(CHANNEL_MAIN_VOLUME);
1655 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1656 updateChannelState(CHANNEL_MUTE);
1657 updateChannelState(CHANNEL_MAIN_MUTE);
1658 updateChannelState(CHANNEL_BASS);
1659 updateChannelState(CHANNEL_MAIN_BASS);
1660 updateChannelState(CHANNEL_TREBLE);
1661 updateChannelState(CHANNEL_MAIN_TREBLE);
1662 updateChannelState(CHANNEL_PLAY_CONTROL);
1663 updateChannelState(CHANNEL_TRACK);
1664 updateChannelState(CHANNEL_FREQUENCY);
1665 updateChannelState(CHANNEL_BRIGHTNESS);
1666 updateChannelState(CHANNEL_TCBYPASS);
1667 updateChannelState(CHANNEL_BALANCE);
1668 updateChannelState(CHANNEL_SPEAKER_A);
1669 updateChannelState(CHANNEL_SPEAKER_B);
1673 * Handle the received information that zone 2 power is ON
1675 private void handlePowerOnZone2() {
1676 boolean prev = powerZone2;
1678 updateChannelState(CHANNEL_ZONE2_POWER);
1680 schedulePowerOnZone2Job();
1685 * Handle the received information that zone 2 power is OFF
1687 private void handlePowerOffZone2() {
1688 cancelPowerOnZone2Job();
1690 updateChannelState(CHANNEL_ZONE2_POWER);
1691 updateChannelState(CHANNEL_ZONE2_SOURCE);
1692 updateChannelState(CHANNEL_ZONE2_VOLUME);
1693 updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1694 updateChannelState(CHANNEL_ZONE2_MUTE);
1698 * Handle the received information that zone 3 power is ON
1700 private void handlePowerOnZone3() {
1701 boolean prev = powerZone3;
1703 updateChannelState(CHANNEL_ZONE3_POWER);
1705 schedulePowerOnZone3Job();
1710 * Handle the received information that zone 3 power is OFF
1712 private void handlePowerOffZone3() {
1713 cancelPowerOnZone3Job();
1715 updateChannelState(CHANNEL_ZONE3_POWER);
1716 updateChannelState(CHANNEL_ZONE3_SOURCE);
1717 updateChannelState(CHANNEL_ZONE3_VOLUME);
1718 updateChannelState(CHANNEL_ZONE3_MUTE);
1722 * Handle the received information that zone 4 power is ON
1724 private void handlePowerOnZone4() {
1725 boolean prev = powerZone4;
1727 updateChannelState(CHANNEL_ZONE4_POWER);
1729 schedulePowerOnZone4Job();
1734 * Handle the received information that zone 4 power is OFF
1736 private void handlePowerOffZone4() {
1737 cancelPowerOnZone4Job();
1739 updateChannelState(CHANNEL_ZONE4_POWER);
1740 updateChannelState(CHANNEL_ZONE4_SOURCE);
1741 updateChannelState(CHANNEL_ZONE4_VOLUME);
1742 updateChannelState(CHANNEL_ZONE4_MUTE);
1746 * Schedule the job that will consider the device as OFF if no new event is received before its running
1748 * @param switchOffAllZones true if all zones have to be considered as OFF
1750 private void schedulePowerOffJob(boolean switchOffAllZones) {
1751 logger.debug("Schedule power OFF job");
1752 cancelPowerOffJob();
1753 powerOffJob = scheduler.schedule(() -> {
1754 logger.debug("Power OFF job");
1756 if (switchOffAllZones) {
1757 handlePowerOffZone2();
1758 handlePowerOffZone3();
1759 handlePowerOffZone4();
1761 }, 2000, TimeUnit.MILLISECONDS);
1765 * Cancel the job that will consider the device as OFF
1767 private void cancelPowerOffJob() {
1768 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1769 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1770 powerOffJob.cancel(true);
1771 this.powerOffJob = null;
1776 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1778 private void schedulePowerOnJob() {
1779 logger.debug("Schedule power ON job");
1781 powerOnJob = scheduler.schedule(() -> {
1782 synchronized (sequenceLock) {
1783 logger.debug("Power ON job");
1785 switch (connector.getProtocol()) {
1787 if (connector.getModel().getRespNbChars() <= 13
1788 && connector.getModel().hasVolumeControl()) {
1789 connector.sendCommand(getVolumeDownCommand());
1791 connector.sendCommand(getVolumeUpCommand());
1794 if (connector.getModel().getNbAdditionalZones() >= 1) {
1795 if (currentZone != 1 && connector.getModel()
1796 .getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1797 selectZone(1, connector.getModel().getZoneSelectCmd());
1798 } else if (!selectingRecord) {
1799 connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1803 connector.sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1806 if (connector.getModel().hasToneControl()) {
1807 if (connector.getModel() == RotelModel.RSX1065) {
1808 // No tone control select command
1809 connector.sendCommand(RotelCommand.TREBLE_DOWN);
1811 connector.sendCommand(RotelCommand.TREBLE_UP);
1813 connector.sendCommand(RotelCommand.BASS_DOWN);
1815 connector.sendCommand(RotelCommand.BASS_UP);
1818 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1823 if (connector.getModel() != RotelModel.RAP1580 && connector.getModel() != RotelModel.RDD1580
1824 && connector.getModel() != RotelModel.RSP1576
1825 && connector.getModel() != RotelModel.RSP1582) {
1826 connector.sendCommand(RotelCommand.UPDATE_AUTO);
1827 Thread.sleep(SLEEP_INTV);
1829 if (connector.getModel().hasSourceControl()) {
1830 connector.sendCommand(RotelCommand.SOURCE);
1831 Thread.sleep(SLEEP_INTV);
1833 if (connector.getModel().hasVolumeControl() || connector.getModel().hasToneControl()) {
1834 if (connector.getModel().hasVolumeControl()
1835 && connector.getModel() != RotelModel.RAP1580
1836 && connector.getModel() != RotelModel.RSP1576
1837 && connector.getModel() != RotelModel.RSP1582) {
1838 connector.sendCommand(RotelCommand.VOLUME_GET_MIN);
1839 Thread.sleep(SLEEP_INTV);
1840 connector.sendCommand(RotelCommand.VOLUME_GET_MAX);
1841 Thread.sleep(SLEEP_INTV);
1843 if (connector.getModel().hasToneControl()) {
1844 connector.sendCommand(RotelCommand.TONE_MAX);
1845 Thread.sleep(SLEEP_INTV);
1847 // Wait enough to be sure to get the min/max values requested just before
1849 if (connector.getModel().hasVolumeControl()) {
1850 connector.sendCommand(RotelCommand.VOLUME_GET);
1851 Thread.sleep(SLEEP_INTV);
1852 if (connector.getModel() != RotelModel.RA11
1853 && connector.getModel() != RotelModel.RA12
1854 && connector.getModel() != RotelModel.RCX1500) {
1855 connector.sendCommand(RotelCommand.MUTE);
1856 Thread.sleep(SLEEP_INTV);
1859 if (connector.getModel().hasToneControl()) {
1860 connector.sendCommand(RotelCommand.BASS);
1861 Thread.sleep(SLEEP_INTV);
1862 connector.sendCommand(RotelCommand.TREBLE);
1863 Thread.sleep(SLEEP_INTV);
1864 connector.sendCommand(RotelCommand.TONE_CONTROLS);
1865 Thread.sleep(SLEEP_INTV);
1868 if (connector.getModel().hasBalanceControl()) {
1869 connector.sendCommand(RotelCommand.BALANCE);
1870 Thread.sleep(SLEEP_INTV);
1872 if (connector.getModel().hasPlayControl()) {
1873 if (connector.getModel() != RotelModel.RCD1570
1874 && connector.getModel() != RotelModel.RCD1572
1875 && (connector.getModel() != RotelModel.RCX1500
1876 || !source.getName().equals("CD"))) {
1877 connector.sendCommand(RotelCommand.PLAY_STATUS);
1878 Thread.sleep(SLEEP_INTV);
1880 connector.sendCommand(RotelCommand.CD_PLAY_STATUS);
1881 Thread.sleep(SLEEP_INTV);
1884 if (connector.getModel().hasDspControl()) {
1885 connector.sendCommand(RotelCommand.DSP_MODE);
1886 Thread.sleep(SLEEP_INTV);
1888 if (connector.getModel().canGetFrequency()) {
1889 connector.sendCommand(RotelCommand.FREQUENCY);
1890 Thread.sleep(SLEEP_INTV);
1892 if (connector.getModel().hasDimmerControl() && connector.getModel().canGetDimmerLevel()) {
1893 connector.sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1894 Thread.sleep(SLEEP_INTV);
1896 if (connector.getModel().hasSpeakerGroups()) {
1897 connector.sendCommand(RotelCommand.SPEAKER);
1898 Thread.sleep(SLEEP_INTV);
1902 connector.sendCommand(RotelCommand.UPDATE_AUTO);
1903 Thread.sleep(SLEEP_INTV);
1904 if (connector.getModel().hasSourceControl()) {
1905 connector.sendCommand(RotelCommand.SOURCE);
1906 Thread.sleep(SLEEP_INTV);
1908 if (connector.getModel().hasVolumeControl()) {
1909 connector.sendCommand(RotelCommand.VOLUME_GET);
1910 Thread.sleep(SLEEP_INTV);
1911 connector.sendCommand(RotelCommand.MUTE);
1912 Thread.sleep(SLEEP_INTV);
1914 if (connector.getModel().hasToneControl()) {
1915 connector.sendCommand(RotelCommand.BASS);
1916 Thread.sleep(SLEEP_INTV);
1917 connector.sendCommand(RotelCommand.TREBLE);
1918 Thread.sleep(SLEEP_INTV);
1919 connector.sendCommand(RotelCommand.TCBYPASS);
1920 Thread.sleep(SLEEP_INTV);
1922 if (connector.getModel().hasBalanceControl()) {
1923 connector.sendCommand(RotelCommand.BALANCE);
1924 Thread.sleep(SLEEP_INTV);
1926 if (connector.getModel().hasPlayControl()) {
1927 connector.sendCommand(RotelCommand.PLAY_STATUS);
1928 Thread.sleep(SLEEP_INTV);
1929 if (source.getName().equals("CD") && !connector.getModel().hasSourceControl()) {
1930 connector.sendCommand(RotelCommand.TRACK);
1931 Thread.sleep(SLEEP_INTV);
1934 if (connector.getModel().hasDspControl()) {
1935 connector.sendCommand(RotelCommand.DSP_MODE);
1936 Thread.sleep(SLEEP_INTV);
1938 if (connector.getModel().canGetFrequency()) {
1939 connector.sendCommand(RotelCommand.FREQUENCY);
1940 Thread.sleep(SLEEP_INTV);
1942 if (connector.getModel().hasDimmerControl() && connector.getModel().canGetDimmerLevel()) {
1943 connector.sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1944 Thread.sleep(SLEEP_INTV);
1946 if (connector.getModel().hasSpeakerGroups()) {
1947 connector.sendCommand(RotelCommand.SPEAKER);
1948 Thread.sleep(SLEEP_INTV);
1952 } catch (RotelException e) {
1953 logger.debug("Init sequence failed: {}", e.getMessage());
1954 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1955 "@text/offline.comm-error-init-sequence");
1957 } catch (InterruptedException e) {
1958 logger.debug("Init sequence interrupted: {}", e.getMessage());
1959 Thread.currentThread().interrupt();
1962 }, 2500, TimeUnit.MILLISECONDS);
1966 * Cancel the job scheduled when the device power (main zone) switched ON
1968 private void cancelPowerOnJob() {
1969 ScheduledFuture<?> powerOnJob = this.powerOnJob;
1970 if (powerOnJob != null && !powerOnJob.isCancelled()) {
1971 powerOnJob.cancel(true);
1972 this.powerOnJob = null;
1977 * Schedule the job to run with a few seconds delay when the zone 2 power switched ON
1979 private void schedulePowerOnZone2Job() {
1980 logger.debug("Schedule power ON zone 2 job");
1981 cancelPowerOnZone2Job();
1982 powerOnZone2Job = scheduler.schedule(() -> {
1983 synchronized (sequenceLock) {
1984 logger.debug("Power ON zone 2 job");
1986 if (connector.getProtocol() == RotelProtocol.HEX
1987 && connector.getModel().getNbAdditionalZones() >= 1) {
1988 selectZone(2, connector.getModel().getZoneSelectCmd());
1989 connector.sendCommand(connector.getModel().hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN
1990 : RotelCommand.VOLUME_DOWN);
1992 connector.sendCommand(connector.getModel().hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP
1993 : RotelCommand.VOLUME_UP);
1996 } catch (RotelException e) {
1997 logger.debug("Init sequence zone 2 failed: {}", e.getMessage());
1998 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1999 "@text/offline.comm-error-init-sequence-zone [\"2\"]");
2001 } catch (InterruptedException e) {
2002 logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage());
2003 Thread.currentThread().interrupt();
2006 }, 2500, TimeUnit.MILLISECONDS);
2010 * Cancel the job scheduled when the zone 2 power switched ON
2012 private void cancelPowerOnZone2Job() {
2013 ScheduledFuture<?> powerOnZone2Job = this.powerOnZone2Job;
2014 if (powerOnZone2Job != null && !powerOnZone2Job.isCancelled()) {
2015 powerOnZone2Job.cancel(true);
2016 this.powerOnZone2Job = null;
2021 * Schedule the job to run with a few seconds delay when the zone 3 power switched ON
2023 private void schedulePowerOnZone3Job() {
2024 logger.debug("Schedule power ON zone 3 job");
2025 cancelPowerOnZone3Job();
2026 powerOnZone3Job = scheduler.schedule(() -> {
2027 synchronized (sequenceLock) {
2028 logger.debug("Power ON zone 3 job");
2030 if (connector.getProtocol() == RotelProtocol.HEX
2031 && connector.getModel().getNbAdditionalZones() >= 2) {
2032 selectZone(3, connector.getModel().getZoneSelectCmd());
2033 connector.sendCommand(connector.getModel().hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN
2034 : RotelCommand.VOLUME_DOWN);
2036 connector.sendCommand(connector.getModel().hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP
2037 : RotelCommand.VOLUME_UP);
2040 } catch (RotelException e) {
2041 logger.debug("Init sequence zone 3 failed: {}", e.getMessage());
2042 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2043 "@text/offline.comm-error-init-sequence-zone [\"3\"]");
2045 } catch (InterruptedException e) {
2046 logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage());
2047 Thread.currentThread().interrupt();
2050 }, 2500, TimeUnit.MILLISECONDS);
2054 * Cancel the job scheduled when the zone 3 power switched ON
2056 private void cancelPowerOnZone3Job() {
2057 ScheduledFuture<?> powerOnZone3Job = this.powerOnZone3Job;
2058 if (powerOnZone3Job != null && !powerOnZone3Job.isCancelled()) {
2059 powerOnZone3Job.cancel(true);
2060 this.powerOnZone3Job = null;
2065 * Schedule the job to run with a few seconds delay when the zone 4 power switched ON
2067 private void schedulePowerOnZone4Job() {
2068 logger.debug("Schedule power ON zone 4 job");
2069 cancelPowerOnZone4Job();
2070 powerOnZone4Job = scheduler.schedule(() -> {
2071 synchronized (sequenceLock) {
2072 logger.debug("Power ON zone 4 job");
2074 if (connector.getProtocol() == RotelProtocol.HEX
2075 && connector.getModel().getNbAdditionalZones() >= 3) {
2076 selectZone(4, connector.getModel().getZoneSelectCmd());
2077 connector.sendCommand(connector.getModel().hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN
2078 : RotelCommand.VOLUME_DOWN);
2080 connector.sendCommand(connector.getModel().hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP
2081 : RotelCommand.VOLUME_UP);
2084 } catch (RotelException e) {
2085 logger.debug("Init sequence zone 4 failed: {}", e.getMessage());
2086 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2087 "@text/offline.comm-error-init-sequence-zone [\"4\"]");
2089 } catch (InterruptedException e) {
2090 logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage());
2091 Thread.currentThread().interrupt();
2094 }, 2500, TimeUnit.MILLISECONDS);
2098 * Cancel the job scheduled when the zone 4 power switched ON
2100 private void cancelPowerOnZone4Job() {
2101 ScheduledFuture<?> powerOnZone4Job = this.powerOnZone4Job;
2102 if (powerOnZone4Job != null && !powerOnZone4Job.isCancelled()) {
2103 powerOnZone4Job.cancel(true);
2104 this.powerOnZone4Job = null;
2109 * Schedule the reconnection job
2111 private void scheduleReconnectJob() {
2112 logger.debug("Schedule reconnect job");
2113 cancelReconnectJob();
2114 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2115 if (!connector.isConnected()) {
2116 logger.debug("Trying to reconnect...");
2119 String error = null;
2120 if (openConnection()) {
2121 synchronized (sequenceLock) {
2122 schedulePowerOffJob(true);
2124 connector.sendCommand(connector.getModel().getPowerStateCmd());
2125 } catch (RotelException e) {
2126 error = "@text/offline.comm-error-first-command-after-reconnection";
2127 logger.debug("First command after connection failed", e);
2128 cancelPowerOffJob();
2133 error = "@text/offline.comm-error-reconnection";
2135 if (error != null) {
2137 handlePowerOffZone2();
2138 handlePowerOffZone3();
2139 handlePowerOffZone4();
2140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2142 updateStatus(ThingStatus.ONLINE);
2145 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2149 * Cancel the reconnection job
2151 private void cancelReconnectJob() {
2152 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2153 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2154 reconnectJob.cancel(true);
2155 this.reconnectJob = null;
2160 * Update the state of a channel
2162 * @param channel the channel
2164 private void updateChannelState(String channel) {
2165 if (!isLinked(channel)) {
2168 State state = UnDefType.UNDEF;
2171 case CHANNEL_MAIN_POWER:
2174 state = OnOffType.from(po.booleanValue());
2177 case CHANNEL_ZONE2_POWER:
2178 state = OnOffType.from(powerZone2);
2180 case CHANNEL_ZONE3_POWER:
2181 state = OnOffType.from(powerZone3);
2183 case CHANNEL_ZONE4_POWER:
2184 state = OnOffType.from(powerZone4);
2186 case CHANNEL_SOURCE:
2187 case CHANNEL_MAIN_SOURCE:
2189 state = new StringType(source.getName());
2192 case CHANNEL_MAIN_RECORD_SOURCE:
2193 RotelSource recordSource = this.recordSource;
2194 if (isPowerOn() && recordSource != null) {
2195 state = new StringType(recordSource.getName());
2198 case CHANNEL_ZONE2_SOURCE:
2199 RotelSource sourceZone2 = this.sourceZone2;
2200 if (powerZone2 && sourceZone2 != null) {
2201 state = new StringType(sourceZone2.getName());
2204 case CHANNEL_ZONE3_SOURCE:
2205 RotelSource sourceZone3 = this.sourceZone3;
2206 if (powerZone3 && sourceZone3 != null) {
2207 state = new StringType(sourceZone3.getName());
2210 case CHANNEL_ZONE4_SOURCE:
2211 RotelSource sourceZone4 = this.sourceZone4;
2212 if (powerZone4 && sourceZone4 != null) {
2213 state = new StringType(sourceZone4.getName());
2217 case CHANNEL_MAIN_DSP:
2219 state = new StringType(dsp.getName());
2222 case CHANNEL_VOLUME:
2223 case CHANNEL_MAIN_VOLUME:
2225 long volumePct = Math
2226 .round((double) (volume - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2227 state = new PercentType(BigDecimal.valueOf(volumePct));
2230 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2232 state = new DecimalType(volume);
2235 case CHANNEL_ZONE2_VOLUME:
2236 if (powerZone2 && !fixedVolumeZone2) {
2237 long volumePct = Math
2238 .round((double) (volumeZone2 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2239 state = new PercentType(BigDecimal.valueOf(volumePct));
2242 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2243 if (powerZone2 && !fixedVolumeZone2) {
2244 state = new DecimalType(volumeZone2);
2247 case CHANNEL_ZONE3_VOLUME:
2248 if (powerZone3 && !fixedVolumeZone3) {
2249 long volumePct = Math
2250 .round((double) (volumeZone3 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2251 state = new PercentType(BigDecimal.valueOf(volumePct));
2254 case CHANNEL_ZONE4_VOLUME:
2255 if (powerZone4 && !fixedVolumeZone4) {
2256 long volumePct = Math
2257 .round((double) (volumeZone4 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2258 state = new PercentType(BigDecimal.valueOf(volumePct));
2262 case CHANNEL_MAIN_MUTE:
2264 state = OnOffType.from(mute);
2267 case CHANNEL_ZONE2_MUTE:
2269 state = OnOffType.from(muteZone2);
2272 case CHANNEL_ZONE3_MUTE:
2274 state = OnOffType.from(muteZone3);
2277 case CHANNEL_ZONE4_MUTE:
2279 state = OnOffType.from(muteZone4);
2283 case CHANNEL_MAIN_BASS:
2285 state = new DecimalType(bass);
2288 case CHANNEL_TREBLE:
2289 case CHANNEL_MAIN_TREBLE:
2291 state = new DecimalType(treble);
2295 if (track > 0 && isPowerOn()) {
2296 state = new DecimalType(track);
2299 case CHANNEL_PLAY_CONTROL:
2301 switch (playStatus) {
2303 state = PlayPauseType.PLAY;
2307 state = PlayPauseType.PAUSE;
2312 case CHANNEL_FREQUENCY:
2313 if (frequency > 0.0 && isPowerOn()) {
2314 state = new DecimalType(frequency);
2318 state = new StringType(frontPanelLine1);
2321 state = new StringType(frontPanelLine2);
2323 case CHANNEL_BRIGHTNESS:
2324 if (isPowerOn() && connector.getModel().hasDimmerControl()) {
2325 long dimmerPct = Math.round((double) (brightness - connector.getModel().getDimmerLevelMin())
2326 / (double) (connector.getModel().getDimmerLevelMax()
2327 - connector.getModel().getDimmerLevelMin())
2329 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2332 case CHANNEL_TCBYPASS:
2334 state = OnOffType.from(tcbypass);
2337 case CHANNEL_BALANCE:
2339 state = new DecimalType(balance);
2342 case CHANNEL_SPEAKER_A:
2344 state = OnOffType.from(speakera);
2347 case CHANNEL_SPEAKER_B:
2349 state = OnOffType.from(speakerb);
2355 updateState(channel, state);
2359 * Inform about the main zone power state
2361 * @return true if main zone power state is known and known as ON
2363 private boolean isPowerOn() {
2364 Boolean power = this.power;
2365 return power != null && power.booleanValue();
2369 * Get the command to be used for main zone POWER ON
2371 * @return the command
2373 private RotelCommand getPowerOnCommand() {
2374 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON
2375 : RotelCommand.POWER_ON;
2379 * Get the command to be used for main zone POWER OFF
2381 * @return the command
2383 private RotelCommand getPowerOffCommand() {
2384 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF
2385 : RotelCommand.POWER_OFF;
2389 * Get the command to be used for main zone VOLUME UP
2391 * @return the command
2393 private RotelCommand getVolumeUpCommand() {
2394 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP
2395 : RotelCommand.VOLUME_UP;
2399 * Get the command to be used for main zone VOLUME DOWN
2401 * @return the command
2403 private RotelCommand getVolumeDownCommand() {
2404 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2405 : RotelCommand.VOLUME_DOWN;
2409 * Get the command to be used for main zone MUTE ON
2411 * @return the command
2413 private RotelCommand getMuteOnCommand() {
2414 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON
2415 : RotelCommand.MUTE_ON;
2419 * Get the command to be used for main zone MUTE OFF
2421 * @return the command
2423 private RotelCommand getMuteOffCommand() {
2424 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF
2425 : RotelCommand.MUTE_OFF;
2429 * Get the command to be used for main zone MUTE TOGGLE
2431 * @return the command
2433 private RotelCommand getMuteToggleCommand() {
2434 return connector.getModel().hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2435 : RotelCommand.MUTE_TOGGLE;