2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.rotel.internal.handler;
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.EventObject;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelBindingConstants;
29 import org.openhab.binding.rotel.internal.RotelException;
30 import org.openhab.binding.rotel.internal.RotelModel;
31 import org.openhab.binding.rotel.internal.RotelPlayStatus;
32 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
33 import org.openhab.binding.rotel.internal.communication.RotelCommand;
34 import org.openhab.binding.rotel.internal.communication.RotelConnector;
35 import org.openhab.binding.rotel.internal.communication.RotelDsp;
36 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
38 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSource;
40 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
41 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
42 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
43 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
44 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
45 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
46 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
47 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
48 import org.openhab.core.io.transport.serial.SerialPortManager;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.IncreaseDecreaseType;
51 import org.openhab.core.library.types.NextPreviousType;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.PercentType;
54 import org.openhab.core.library.types.PlayPauseType;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.StateOption;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
72 * @author Laurent Garnier - Initial contribution
75 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
77 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
79 private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
80 private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
81 private static final boolean USE_SIMULATED_DEVICE = false;
82 private static final int SLEEP_INTV = 30;
84 private @Nullable ScheduledFuture<?> reconnectJob;
85 private @Nullable ScheduledFuture<?> powerOnJob;
86 private @Nullable ScheduledFuture<?> powerOffJob;
87 private @Nullable ScheduledFuture<?> powerOnZone2Job;
88 private @Nullable ScheduledFuture<?> powerOnZone3Job;
89 private @Nullable ScheduledFuture<?> powerOnZone4Job;
91 private RotelStateDescriptionOptionProvider stateDescriptionProvider;
92 private SerialPortManager serialPortManager;
94 private RotelModel model;
95 private RotelProtocol protocol;
96 private RotelAbstractProtocolHandler protocolHandler;
97 private RotelConnector connector;
99 private int minVolume;
100 private int maxVolume;
101 private int minToneLevel;
102 private int maxToneLevel;
104 private int currentZone = 1;
105 private boolean selectingRecord;
106 private @Nullable Boolean power;
107 private boolean powerZone2;
108 private boolean powerZone3;
109 private boolean powerZone4;
110 private RotelSource source = RotelSource.CAT0_CD;
111 private @Nullable RotelSource recordSource;
112 private @Nullable RotelSource sourceZone2;
113 private @Nullable RotelSource sourceZone3;
114 private @Nullable RotelSource sourceZone4;
115 private RotelDsp dsp = RotelDsp.CAT1_NONE;
117 private boolean mute;
118 private boolean fixedVolumeZone2;
119 private int volumeZone2;
120 private boolean muteZone2;
121 private boolean fixedVolumeZone3;
122 private int volumeZone3;
123 private boolean muteZone3;
124 private boolean fixedVolumeZone4;
125 private int volumeZone4;
126 private boolean muteZone4;
129 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
131 private double frequency;
132 private String frontPanelLine1 = "";
133 private String frontPanelLine2 = "";
134 private int brightness;
135 private boolean tcbypass;
137 private int minBalanceLevel;
138 private int maxBalanceLevel;
139 private boolean speakera;
140 private boolean speakerb;
142 private Object sequenceLock = new Object();
147 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
148 SerialPortManager serialPortManager) {
150 this.stateDescriptionProvider = stateDescriptionProvider;
151 this.serialPortManager = serialPortManager;
152 this.model = DEFAULT_MODEL;
153 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
154 this.protocol = protocolHandler.getProtocol();
155 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
159 public void initialize() {
160 logger.debug("Start initializing handler for thing {}", getThing().getUID());
162 switch (getThing().getThingTypeUID().getId()) {
163 case THING_TYPE_ID_RSP1066:
164 model = RotelModel.RSP1066;
166 case THING_TYPE_ID_RSP1068:
167 model = RotelModel.RSP1068;
169 case THING_TYPE_ID_RSP1069:
170 model = RotelModel.RSP1069;
172 case THING_TYPE_ID_RSP1098:
173 model = RotelModel.RSP1098;
175 case THING_TYPE_ID_RSP1570:
176 model = RotelModel.RSP1570;
178 case THING_TYPE_ID_RSP1572:
179 model = RotelModel.RSP1572;
181 case THING_TYPE_ID_RSX1055:
182 model = RotelModel.RSX1055;
184 case THING_TYPE_ID_RSX1056:
185 model = RotelModel.RSX1056;
187 case THING_TYPE_ID_RSX1057:
188 model = RotelModel.RSX1057;
190 case THING_TYPE_ID_RSX1058:
191 model = RotelModel.RSX1058;
193 case THING_TYPE_ID_RSX1065:
194 model = RotelModel.RSX1065;
196 case THING_TYPE_ID_RSX1067:
197 model = RotelModel.RSX1067;
199 case THING_TYPE_ID_RSX1550:
200 model = RotelModel.RSX1550;
202 case THING_TYPE_ID_RSX1560:
203 model = RotelModel.RSX1560;
205 case THING_TYPE_ID_RSX1562:
206 model = RotelModel.RSX1562;
208 case THING_TYPE_ID_A11:
209 model = RotelModel.A11;
211 case THING_TYPE_ID_A12:
212 model = RotelModel.A12;
214 case THING_TYPE_ID_A14:
215 model = RotelModel.A14;
217 case THING_TYPE_ID_CD11:
218 model = RotelModel.CD11;
220 case THING_TYPE_ID_CD14:
221 model = RotelModel.CD14;
223 case THING_TYPE_ID_RA11:
224 model = RotelModel.RA11;
226 case THING_TYPE_ID_RA12:
227 model = RotelModel.RA12;
229 case THING_TYPE_ID_RA1570:
230 model = RotelModel.RA1570;
232 case THING_TYPE_ID_RA1572:
233 model = RotelModel.RA1572;
235 case THING_TYPE_ID_RA1592:
236 model = RotelModel.RA1592;
238 case THING_TYPE_ID_RAP1580:
239 model = RotelModel.RAP1580;
241 case THING_TYPE_ID_RC1570:
242 model = RotelModel.RC1570;
244 case THING_TYPE_ID_RC1572:
245 model = RotelModel.RC1572;
247 case THING_TYPE_ID_RC1590:
248 model = RotelModel.RC1590;
250 case THING_TYPE_ID_RCD1570:
251 model = RotelModel.RCD1570;
253 case THING_TYPE_ID_RCD1572:
254 model = RotelModel.RCD1572;
256 case THING_TYPE_ID_RCX1500:
257 model = RotelModel.RCX1500;
259 case THING_TYPE_ID_RDD1580:
260 model = RotelModel.RDD1580;
262 case THING_TYPE_ID_RDG1520:
263 case THING_TYPE_ID_RT09:
264 model = RotelModel.RDG1520;
266 case THING_TYPE_ID_RSP1576:
267 model = RotelModel.RSP1576;
269 case THING_TYPE_ID_RSP1582:
270 model = RotelModel.RSP1582;
272 case THING_TYPE_ID_RT11:
273 model = RotelModel.RT11;
275 case THING_TYPE_ID_RT1570:
276 model = RotelModel.RT1570;
278 case THING_TYPE_ID_T11:
279 model = RotelModel.T11;
281 case THING_TYPE_ID_T14:
282 model = RotelModel.T14;
284 case THING_TYPE_ID_P5:
285 model = RotelModel.P5;
287 case THING_TYPE_ID_X3:
288 model = RotelModel.X3;
290 case THING_TYPE_ID_X5:
291 model = RotelModel.X5;
294 model = DEFAULT_MODEL;
298 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
300 protocol = RotelProtocol.HEX;
301 if (config.protocol != null && !config.protocol.isEmpty()) {
303 protocol = RotelProtocol.getFromName(config.protocol);
304 } catch (RotelException e) {
305 // Invalid protocol name in configuration, HEX will be considered by default
308 Map<String, String> properties = editProperties();
309 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
310 if (property != null && !property.isEmpty()) {
312 protocol = RotelProtocol.getFromName(property);
313 } catch (RotelException e) {
314 // Invalid protocol name in thing property, HEX will be considered by default
318 logger.debug("rotelProtocol {}", protocol.getName());
320 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
321 Map<RotelSource, String> sourcesLabels = new HashMap<>();
323 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
325 if (model.hasVolumeControl()) {
326 maxVolume = model.getVolumeMax();
327 if (!model.hasDirectVolumeControl()) {
329 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
330 minVolume, maxVolume);
333 if (model.hasToneControl()) {
334 maxToneLevel = model.getToneLevelMax();
335 minToneLevel = -maxToneLevel;
337 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
338 minToneLevel, maxToneLevel);
340 if (model.hasBalanceControl()) {
341 maxBalanceLevel = model.getBalanceLevelMax();
342 minBalanceLevel = -maxBalanceLevel;
343 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
344 minBalanceLevel, maxBalanceLevel);
347 // Check configuration settings
348 String configError = null;
349 if ((config.serialPort == null || config.serialPort.isEmpty())
350 && (config.host == null || config.host.isEmpty())) {
351 configError = "@text/offline.config-error-unknown-serialport-and-host";
352 } else if (config.host == null || config.host.isEmpty()) {
353 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
354 configError = "@text/offline.config-error-invalid-serial-over-ip";
357 if (config.port == null) {
358 configError = "@text/offline.config-error-unknown-port";
359 } else if (config.port <= 0) {
360 configError = "@text/offline.config-error-invalid-port";
364 if (configError != null) {
365 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
367 for (RotelSource src : model.getSources()) {
368 // Consider custom input labels
370 switch (src.getName()) {
372 label = config.inputLabelCd;
375 label = config.inputLabelTuner;
378 label = config.inputLabelTape;
381 label = config.inputLabelPhono;
384 label = config.inputLabelVideo1;
387 label = config.inputLabelVideo2;
390 label = config.inputLabelVideo3;
393 label = config.inputLabelVideo4;
396 label = config.inputLabelVideo5;
399 label = config.inputLabelVideo6;
402 label = config.inputLabelUsb;
405 label = config.inputLabelMulti;
410 if (label != null && !label.isEmpty()) {
411 sourcesCustomLabels.put(src, label);
413 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
416 if (protocol == RotelProtocol.HEX) {
417 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
418 } else if (protocol == RotelProtocol.ASCII_V1) {
419 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
421 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
424 if (USE_SIMULATED_DEVICE) {
425 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
426 } else if (config.serialPort != null) {
427 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
428 protocolHandler, readerThreadName);
430 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
433 if (model.hasSourceControl()) {
434 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
435 getStateOptions(model.getSources(), sourcesCustomLabels));
436 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
437 getStateOptions(model.getSources(), sourcesCustomLabels));
438 stateDescriptionProvider.setStateOptions(
439 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
440 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
442 if (model.hasZone2SourceControl()) {
443 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
444 getStateOptions(model.getZone2Sources(), sourcesCustomLabels));
446 if (model.hasZone3SourceControl()) {
447 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
448 getStateOptions(model.getZone3Sources(), sourcesCustomLabels));
450 if (model.hasZone4SourceControl()) {
451 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
452 getStateOptions(model.getZone4Sources(), sourcesCustomLabels));
454 if (model.hasDspControl()) {
455 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
456 model.getDspStateOptions());
457 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
458 model.getDspStateOptions());
461 updateStatus(ThingStatus.UNKNOWN);
463 scheduleReconnectJob();
466 logger.debug("Finished initializing!");
470 public void dispose() {
471 logger.debug("Disposing handler for thing {}", getThing().getUID());
474 cancelPowerOnZone2Job();
475 cancelPowerOnZone3Job();
476 cancelPowerOnZone4Job();
477 cancelReconnectJob();
482 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
483 List<StateOption> options = new ArrayList<>();
484 for (RotelSource item : list) {
485 String label = sourcesLabels.get(item);
486 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
492 public void handleCommand(ChannelUID channelUID, Command command) {
493 String channel = channelUID.getId();
495 if (getThing().getStatus() != ThingStatus.ONLINE) {
496 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
500 if (command instanceof RefreshType) {
501 updateChannelState(channel);
505 if (!connector.isConnected()) {
506 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
512 boolean success = true;
513 synchronized (sequenceLock) {
517 case CHANNEL_MAIN_POWER:
518 handlePowerCmd(channel, command, getPowerOnCommand(), getPowerOffCommand());
520 case CHANNEL_ZONE2_POWER:
521 if (model.hasZone2Commands()) {
522 handlePowerCmd(channel, command, RotelCommand.ZONE2_POWER_ON, RotelCommand.ZONE2_POWER_OFF);
523 } else if (model.getNbAdditionalZones() == 1) {
524 if (isPowerOn() || powerZone2) {
525 selectZone(2, model.getZoneSelectCmd());
527 sendCommand(RotelCommand.ZONE_SELECT);
530 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
533 case CHANNEL_ZONE3_POWER:
534 if (model.hasZone3Commands()) {
535 handlePowerCmd(channel, command, RotelCommand.ZONE3_POWER_ON, RotelCommand.ZONE3_POWER_OFF);
538 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
541 case CHANNEL_ZONE4_POWER:
542 if (model.hasZone4Commands()) {
543 handlePowerCmd(channel, command, RotelCommand.ZONE4_POWER_ON, RotelCommand.ZONE4_POWER_OFF);
546 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
550 case CHANNEL_MAIN_SOURCE:
553 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
555 src = model.getSourceFromName(command.toString());
556 cmd = model.hasOtherThanPrimaryCommands() ? src.getMainZoneCommand() : src.getCommand();
559 if (model.canGetFrequency()) {
560 // send <new-source> returns
561 // 1.) the selected <new-source>
562 // 2.) the used frequency
564 // at response-time the frequency has the value of <old-source>
565 // so we must wait a short moment to get the frequency of <new-source>
567 sendCommand(RotelCommand.FREQUENCY);
569 updateChannelState(CHANNEL_FREQUENCY);
573 logger.debug("Command {} from channel {} failed: undefined source command", command,
578 case CHANNEL_MAIN_RECORD_SOURCE:
581 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
582 } else if (model.hasOtherThanPrimaryCommands()) {
583 src = model.getSourceFromName(command.toString());
584 cmd = src.getRecordCommand();
589 logger.debug("Command {} from channel {} failed: undefined record source command",
593 src = model.getSourceFromName(command.toString());
594 cmd = src.getCommand();
596 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
601 logger.debug("Command {} from channel {} failed: undefined source command", command,
606 case CHANNEL_ZONE2_SOURCE:
609 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
610 } else if (model.hasZone2Commands()) {
611 src = model.getSourceFromName(command.toString());
612 cmd = src.getZone2Command();
617 logger.debug("Command {} from channel {} failed: undefined zone 2 source command",
620 } else if (model.getNbAdditionalZones() >= 1) {
621 src = model.getSourceFromName(command.toString());
622 cmd = src.getCommand();
624 selectZone(2, model.getZoneSelectCmd());
628 logger.debug("Command {} from channel {} failed: undefined source command", command,
633 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
636 case CHANNEL_ZONE3_SOURCE:
639 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
640 } else if (model.hasZone3Commands()) {
641 src = model.getSourceFromName(command.toString());
642 cmd = src.getZone3Command();
647 logger.debug("Command {} from channel {} failed: undefined zone 3 source command",
652 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
655 case CHANNEL_ZONE4_SOURCE:
658 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
659 } else if (model.hasZone4Commands()) {
660 src = model.getSourceFromName(command.toString());
661 cmd = src.getZone4Command();
666 logger.debug("Command {} from channel {} failed: undefined zone 4 source command",
671 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
675 case CHANNEL_MAIN_DSP:
678 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
680 sendCommand(model.getCommandFromDspName(command.toString()));
684 case CHANNEL_MAIN_VOLUME:
687 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
688 } else if (model.hasVolumeControl()) {
689 handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
690 RotelCommand.VOLUME_SET);
693 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
696 case CHANNEL_MAIN_VOLUME_UP_DOWN:
699 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
700 } else if (model.hasVolumeControl()) {
701 handleVolumeCmd(volume, channel, command, getVolumeUpCommand(), getVolumeDownCommand(),
705 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
708 case CHANNEL_ZONE2_VOLUME:
711 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
712 } else if (fixedVolumeZone2) {
714 logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
716 } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
717 if (model.hasZone2Commands()) {
718 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
719 RotelCommand.ZONE2_VOLUME_DOWN, RotelCommand.ZONE2_VOLUME_SET);
721 selectZone(2, model.getZoneSelectCmd());
722 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
723 RotelCommand.VOLUME_DOWN, RotelCommand.VOLUME_SET);
727 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
730 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
733 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
734 } else if (fixedVolumeZone2) {
736 logger.debug("Command {} from channel {} ignored: fixed volume in zone 2", command,
738 } else if (model.hasVolumeControl() && model.getNbAdditionalZones() >= 1) {
739 if (model.hasZone2Commands()) {
740 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.ZONE2_VOLUME_UP,
741 RotelCommand.ZONE2_VOLUME_DOWN, null);
743 selectZone(2, model.getZoneSelectCmd());
744 handleVolumeCmd(volumeZone2, channel, command, RotelCommand.VOLUME_UP,
745 RotelCommand.VOLUME_DOWN, null);
749 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
752 case CHANNEL_ZONE3_VOLUME:
755 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
756 } else if (fixedVolumeZone3) {
758 logger.debug("Command {} from channel {} ignored: fixed volume in zone 3", command,
760 } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
761 handleVolumeCmd(volumeZone3, channel, command, RotelCommand.ZONE3_VOLUME_UP,
762 RotelCommand.ZONE3_VOLUME_DOWN, RotelCommand.ZONE3_VOLUME_SET);
765 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
768 case CHANNEL_ZONE4_VOLUME:
771 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
772 } else if (fixedVolumeZone4) {
774 logger.debug("Command {} from channel {} ignored: fixed volume in zone 4", command,
776 } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
777 handleVolumeCmd(volumeZone4, channel, command, RotelCommand.ZONE4_VOLUME_UP,
778 RotelCommand.ZONE4_VOLUME_DOWN, RotelCommand.ZONE4_VOLUME_SET);
781 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
785 case CHANNEL_MAIN_MUTE:
788 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
789 } else if (model.hasVolumeControl()) {
790 handleMuteCmd(protocol == RotelProtocol.HEX, channel, command, getMuteOnCommand(),
791 getMuteOffCommand(), getMuteToggleCommand());
794 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
797 case CHANNEL_ZONE2_MUTE:
800 logger.debug("Command {} from channel {} ignored: zone 2 in standby", command, channel);
801 } else if (model.hasVolumeControl() && model.hasZone2Commands()) {
802 handleMuteCmd(false, channel, command, RotelCommand.ZONE2_MUTE_ON,
803 RotelCommand.ZONE2_MUTE_OFF, RotelCommand.ZONE2_MUTE_TOGGLE);
806 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
809 case CHANNEL_ZONE3_MUTE:
812 logger.debug("Command {} from channel {} ignored: zone 3 in standby", command, channel);
813 } else if (model.hasVolumeControl() && model.hasZone3Commands()) {
814 handleMuteCmd(false, channel, command, RotelCommand.ZONE3_MUTE_ON,
815 RotelCommand.ZONE3_MUTE_OFF, RotelCommand.ZONE3_MUTE_TOGGLE);
818 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
821 case CHANNEL_ZONE4_MUTE:
824 logger.debug("Command {} from channel {} ignored: zone 4 in standby", command, channel);
825 } else if (model.hasVolumeControl() && model.hasZone4Commands()) {
826 handleMuteCmd(false, channel, command, RotelCommand.ZONE4_MUTE_ON,
827 RotelCommand.ZONE4_MUTE_OFF, RotelCommand.ZONE4_MUTE_TOGGLE);
830 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
834 case CHANNEL_MAIN_BASS:
837 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
838 } else if (tcbypass) {
839 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
841 updateChannelState(CHANNEL_BASS);
843 handleToneCmd(bass, channel, command, 2, RotelCommand.BASS_UP, RotelCommand.BASS_DOWN,
844 RotelCommand.BASS_SET);
848 case CHANNEL_MAIN_TREBLE:
851 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
852 } else if (tcbypass) {
853 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
855 updateChannelState(CHANNEL_TREBLE);
857 handleToneCmd(treble, channel, command, 1, RotelCommand.TREBLE_UP, RotelCommand.TREBLE_DOWN,
858 RotelCommand.TREBLE_SET);
861 case CHANNEL_PLAY_CONTROL:
864 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
865 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
866 sendCommand(RotelCommand.PLAY);
867 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
868 sendCommand(RotelCommand.PAUSE);
869 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
870 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
871 Thread.sleep(SLEEP_INTV);
872 sendCommand(RotelCommand.PLAY_STATUS);
874 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
875 sendCommand(RotelCommand.TRACK_FORWARD);
876 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
877 sendCommand(RotelCommand.TRACK_BACKWORD);
880 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
883 case CHANNEL_BRIGHTNESS:
886 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
887 } else if (!model.hasDimmerControl()) {
889 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
890 } else if (command instanceof PercentType) {
891 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
892 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
893 + model.getDimmerLevelMin();
894 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
897 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
900 case CHANNEL_TCBYPASS:
903 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
904 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
906 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
908 handleTcbypassCmd(channel, command,
909 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
910 : RotelCommand.TCBYPASS_ON,
911 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
912 : RotelCommand.TCBYPASS_OFF);
915 case CHANNEL_BALANCE:
918 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
919 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
921 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
923 handleBalanceCmd(channel, command, RotelCommand.BALANCE_LEFT, RotelCommand.BALANCE_RIGHT,
924 RotelCommand.BALANCE_SET);
927 case CHANNEL_SPEAKER_A:
930 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
932 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
933 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
936 case CHANNEL_SPEAKER_B:
939 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
941 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
942 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
947 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
951 logger.debug("Command {} from channel {} succeeded", command, channel);
953 updateChannelState(channel);
955 } catch (RotelException e) {
956 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
957 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
958 "@text/offline.comm-error-sending-command");
960 scheduleReconnectJob();
961 } catch (InterruptedException e) {
962 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
963 Thread.currentThread().interrupt();
969 * Handle a power ON/OFF command
971 * @param channel the channel
972 * @param command the received channel command (OnOffType)
973 * @param onCmd the command to be sent to the device to power it ON
974 * @param offCmd the command to be sent to the device to power it OFF
976 * @throws RotelException in case of communication error with the device
978 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
979 throws RotelException {
980 if (command instanceof OnOffType && command == OnOffType.ON) {
982 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
985 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
990 * Handle a volume command
992 * @param current the current volume
993 * @param channel the channel
994 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
995 * @param upCmd the command to be sent to the device to increase the volume
996 * @param downCmd the command to be sent to the device to decrease the volume
997 * @param setCmd the command to be sent to the device to set the volume at a value
999 * @throws RotelException in case of communication error with the device
1001 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
1002 @Nullable RotelCommand setCmd) throws RotelException {
1003 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1005 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1006 sendCommand(downCmd);
1007 } else if (command instanceof DecimalType && setCmd == null) {
1008 int value = ((DecimalType) command).intValue();
1009 if (value >= minVolume && value <= maxVolume) {
1010 if (value > current) {
1012 } else if (value < current) {
1013 sendCommand(downCmd);
1016 } else if (command instanceof PercentType && setCmd != null) {
1017 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1019 sendCommand(setCmd, value);
1021 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1026 * Handle a mute command
1028 * @param onlyToggle true if only the toggle command must be used
1029 * @param channel the channel
1030 * @param command the received channel command (OnOffType)
1031 * @param onCmd the command to be sent to the device to mute
1032 * @param offCmd the command to be sent to the device to unmute
1033 * @param toggleCmd the command to be sent to the device to toggle the mute state
1035 * @throws RotelException in case of communication error with the device
1037 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1038 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1039 if (command instanceof OnOffType) {
1041 sendCommand(toggleCmd);
1042 } else if (command == OnOffType.ON) {
1044 } else if (command == OnOffType.OFF) {
1045 sendCommand(offCmd);
1048 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1053 * Handle a tone level adjustment command (bass or treble)
1055 * @param current the current tone level
1056 * @param channel the channel
1057 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1058 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1059 * @param upCmd the command to be sent to the device to increase the tone level
1060 * @param downCmd the command to be sent to the device to decrease the tone level
1061 * @param setCmd the command to be sent to the device to set the tone level at a value
1063 * @throws RotelException in case of communication error with the device
1064 * @throws InterruptedException in case of interruption during a thread sleep
1066 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1067 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1068 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1069 selectToneControl(nbSelect);
1071 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1072 selectToneControl(nbSelect);
1073 sendCommand(downCmd);
1074 } else if (command instanceof DecimalType) {
1075 int value = ((DecimalType) command).intValue();
1076 if (value >= minToneLevel && value <= maxToneLevel) {
1077 if (protocol != RotelProtocol.HEX) {
1078 sendCommand(setCmd, value);
1079 } else if (value > current) {
1080 selectToneControl(nbSelect);
1082 } else if (value < current) {
1083 selectToneControl(nbSelect);
1084 sendCommand(downCmd);
1088 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1093 * Handle a tcbypass command (only for ASCII protocol)
1095 * @param channel the channel
1096 * @param command the received channel command (OnOffType)
1097 * @param onCmd the command to be sent to the device to bypass_on
1098 * @param offCmd the command to be sent to the device to bypass_off
1100 * @throws RotelException in case of communication error with the device
1102 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1103 throws RotelException, InterruptedException {
1104 if (command instanceof OnOffType) {
1105 if (command == OnOffType.ON) {
1109 updateChannelState(CHANNEL_BASS);
1110 updateChannelState(CHANNEL_TREBLE);
1111 } else if (command == OnOffType.OFF) {
1112 sendCommand(offCmd);
1114 sendCommand(RotelCommand.BASS);
1116 sendCommand(RotelCommand.TREBLE);
1119 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1124 * Handle a speaker command
1126 * @param onlyToggle true if only the toggle command must be used
1127 * @param channel the channel
1128 * @param command the received channel command (OnOffType)
1129 * @param onCmd the command to be sent to the device to speaker_x_on
1130 * @param offCmd the command to be sent to the device to speaker_x_off
1131 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1133 * @throws RotelException in case of communication error with the device
1135 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1136 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1137 if (command instanceof OnOffType) {
1139 sendCommand(toggleCmd);
1140 } else if (command == OnOffType.ON) {
1142 } else if (command == OnOffType.OFF) {
1143 sendCommand(offCmd);
1146 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1151 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1153 * @param channel the channel
1154 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1155 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1156 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1157 * @param setCmd the command to be sent to the device to set the balance at a value
1159 * @throws RotelException in case of communication error with the device
1160 * @throws InterruptedException in case of interruption during a thread sleep
1162 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1163 RotelCommand setCmd) throws RotelException, InterruptedException {
1164 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1165 sendCommand(rightCmd);
1166 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1167 sendCommand(leftCmd);
1168 } else if (command instanceof DecimalType) {
1169 int value = ((DecimalType) command).intValue();
1170 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1171 sendCommand(setCmd, value);
1174 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1179 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1181 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1183 * @throws RotelException in case of communication error with the device
1184 * @throws InterruptedException in case of interruption during a thread sleep
1186 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1187 // No tone control select command for RSX-1065
1188 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1189 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1194 * Run a sequence of commands to display a particular zone on the device front panel
1196 * @param zone the zone to be displayed (1 for main zone)
1197 * @param selectCommand the command to be sent to the device to switch the display between zones
1199 * @throws RotelException in case of communication error with the device
1200 * @throws InterruptedException in case of interruption during a thread sleep
1202 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1203 throws RotelException, InterruptedException {
1204 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1 && zone >= 1 && zone != currentZone
1205 && selectCommand != null) {
1207 if (zone < currentZone) {
1208 nbSelect = zone + model.getNbAdditionalZones() - currentZone;
1209 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1213 nbSelect = zone - currentZone;
1214 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1215 && !selectingRecord) {
1219 selectFeature(nbSelect, null, selectCommand);
1224 * Run a sequence of commands to display a particular feature on the device front panel
1226 * @param nbSelect the number of select commands to be run
1227 * @param preCmd the initial command to be sent to the device (before the select commands)
1228 * @param selectCmd the select command to be sent to the device
1230 * @throws RotelException in case of communication error with the device
1231 * @throws InterruptedException in case of interruption during a thread sleep
1233 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1234 throws RotelException, InterruptedException {
1235 if (protocol == RotelProtocol.HEX) {
1236 if (preCmd != null) {
1237 sendCommand(preCmd);
1240 for (int i = 1; i <= nbSelect; i++) {
1241 sendCommand(selectCmd);
1248 * Open the connection with the Rotel device
1250 * @return true if the connection is opened successfully or flase if not
1252 private synchronized boolean openConnection() {
1253 protocolHandler.addEventListener(this);
1256 } catch (RotelException e) {
1257 logger.debug("openConnection() failed", e);
1259 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1260 return connector.isConnected();
1264 * Close the connection with the Rotel device
1266 private synchronized void closeConnection() {
1268 protocolHandler.removeEventListener(this);
1269 logger.debug("closeConnection(): disconnected");
1273 public void onNewMessageEvent(EventObject event) {
1274 cancelPowerOffJob();
1276 RotelMessageEvent evt = (RotelMessageEvent) event;
1277 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1279 String key = evt.getKey();
1280 String value = evt.getValue().trim();
1281 if (!KEY_ERROR.equals(key)) {
1282 updateStatus(ThingStatus.ONLINE);
1287 logger.debug("Reading feedback message failed");
1288 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1289 "@text/offline.comm-error-reading-thread");
1293 frontPanelLine1 = value;
1294 updateChannelState(CHANNEL_LINE1);
1297 frontPanelLine2 = value;
1298 updateChannelState(CHANNEL_LINE2);
1301 currentZone = Integer.parseInt(value);
1303 case KEY_RECORD_SEL:
1304 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1307 if (POWER_ON.equalsIgnoreCase(value)) {
1309 } else if (STANDBY.equalsIgnoreCase(value)) {
1311 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1312 schedulePowerOffJob(false);
1314 throw new RotelException("Invalid value");
1317 case KEY_POWER_ZONE2:
1318 if (POWER_ON.equalsIgnoreCase(value)) {
1319 handlePowerOnZone2();
1320 } else if (STANDBY.equalsIgnoreCase(value)) {
1321 handlePowerOffZone2();
1323 throw new RotelException("Invalid value");
1326 case KEY_POWER_ZONE3:
1327 if (POWER_ON.equalsIgnoreCase(value)) {
1328 handlePowerOnZone3();
1329 } else if (STANDBY.equalsIgnoreCase(value)) {
1330 handlePowerOffZone3();
1332 throw new RotelException("Invalid value");
1335 case KEY_POWER_ZONE4:
1336 if (POWER_ON.equalsIgnoreCase(value)) {
1337 handlePowerOnZone4();
1338 } else if (STANDBY.equalsIgnoreCase(value)) {
1339 handlePowerOffZone4();
1341 throw new RotelException("Invalid value");
1344 case KEY_VOLUME_MIN:
1345 minVolume = Integer.parseInt(value);
1346 if (!model.hasDirectVolumeControl()) {
1347 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1351 case KEY_VOLUME_MAX:
1352 maxVolume = Integer.parseInt(value);
1353 if (!model.hasDirectVolumeControl()) {
1354 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1359 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1361 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1364 volume = Integer.parseInt(value);
1366 updateChannelState(CHANNEL_VOLUME);
1367 updateChannelState(CHANNEL_MAIN_VOLUME);
1368 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1371 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1373 updateChannelState(CHANNEL_MUTE);
1374 updateChannelState(CHANNEL_MAIN_MUTE);
1375 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1377 updateChannelState(CHANNEL_MUTE);
1378 updateChannelState(CHANNEL_MAIN_MUTE);
1380 throw new RotelException("Invalid value");
1383 case KEY_VOLUME_ZONE2:
1384 fixedVolumeZone2 = false;
1385 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1386 fixedVolumeZone2 = true;
1387 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1388 volumeZone2 = minVolume;
1389 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1390 volumeZone2 = maxVolume;
1392 volumeZone2 = Integer.parseInt(value);
1394 updateChannelState(CHANNEL_ZONE2_VOLUME);
1395 updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1397 case KEY_VOLUME_ZONE3:
1398 fixedVolumeZone3 = false;
1399 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1400 fixedVolumeZone3 = true;
1401 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1402 volumeZone3 = minVolume;
1403 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1404 volumeZone3 = maxVolume;
1406 volumeZone3 = Integer.parseInt(value);
1408 updateChannelState(CHANNEL_ZONE3_VOLUME);
1410 case KEY_VOLUME_ZONE4:
1411 fixedVolumeZone4 = false;
1412 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1413 fixedVolumeZone4 = true;
1414 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1415 volumeZone4 = minVolume;
1416 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1417 volumeZone4 = maxVolume;
1419 volumeZone4 = Integer.parseInt(value);
1421 updateChannelState(CHANNEL_ZONE4_VOLUME);
1423 case KEY_MUTE_ZONE2:
1424 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1426 updateChannelState(CHANNEL_ZONE2_MUTE);
1427 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1429 updateChannelState(CHANNEL_ZONE2_MUTE);
1431 throw new RotelException("Invalid value");
1434 case KEY_MUTE_ZONE3:
1435 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1437 updateChannelState(CHANNEL_ZONE3_MUTE);
1438 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1440 updateChannelState(CHANNEL_ZONE3_MUTE);
1442 throw new RotelException("Invalid value");
1445 case KEY_MUTE_ZONE4:
1446 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1448 updateChannelState(CHANNEL_ZONE4_MUTE);
1449 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1451 updateChannelState(CHANNEL_ZONE4_MUTE);
1453 throw new RotelException("Invalid value");
1457 maxToneLevel = Integer.parseInt(value);
1458 minToneLevel = -maxToneLevel;
1460 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1461 minToneLevel, maxToneLevel);
1464 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1465 bass = minToneLevel;
1466 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1467 bass = maxToneLevel;
1469 bass = Integer.parseInt(value);
1471 updateChannelState(CHANNEL_BASS);
1472 updateChannelState(CHANNEL_MAIN_BASS);
1475 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1476 treble = minToneLevel;
1477 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1478 treble = maxToneLevel;
1480 treble = Integer.parseInt(value);
1482 updateChannelState(CHANNEL_TREBLE);
1483 updateChannelState(CHANNEL_MAIN_TREBLE);
1486 source = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1487 updateChannelState(CHANNEL_SOURCE);
1488 updateChannelState(CHANNEL_MAIN_SOURCE);
1491 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1492 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1494 case KEY_SOURCE_ZONE2:
1495 sourceZone2 = model.getZone2SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1496 updateChannelState(CHANNEL_ZONE2_SOURCE);
1498 case KEY_SOURCE_ZONE3:
1499 sourceZone3 = model.getZone3SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1500 updateChannelState(CHANNEL_ZONE3_SOURCE);
1502 case KEY_SOURCE_ZONE4:
1503 sourceZone4 = model.getZone4SourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1504 updateChannelState(CHANNEL_ZONE4_SOURCE);
1507 if ("dolby_pliix_movie".equals(value)) {
1508 value = "dolby_plii_movie";
1509 } else if ("dolby_pliix_music".equals(value)) {
1510 value = "dolby_plii_music";
1511 } else if ("dolby_pliix_game".equals(value)) {
1512 value = "dolby_plii_game";
1514 dsp = model.getDspFromFeedback(value);
1515 logger.debug("DSP {}", dsp.getName());
1516 updateChannelState(CHANNEL_DSP);
1517 updateChannelState(CHANNEL_MAIN_DSP);
1519 case KEY1_PLAY_STATUS:
1520 case KEY2_PLAY_STATUS:
1521 if (PLAY.equalsIgnoreCase(value)) {
1522 playStatus = RotelPlayStatus.PLAYING;
1523 updateChannelState(CHANNEL_PLAY_CONTROL);
1524 } else if (PAUSE.equalsIgnoreCase(value)) {
1525 playStatus = RotelPlayStatus.PAUSED;
1526 updateChannelState(CHANNEL_PLAY_CONTROL);
1527 } else if (STOP.equalsIgnoreCase(value)) {
1528 playStatus = RotelPlayStatus.STOPPED;
1529 updateChannelState(CHANNEL_PLAY_CONTROL);
1531 throw new RotelException("Invalid value");
1535 if (source.getName().equals("CD") && !model.hasSourceControl()) {
1536 track = Integer.parseInt(value);
1537 updateChannelState(CHANNEL_TRACK);
1541 if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1544 // Suppress a potential ending "k" or "K"
1545 if (value.toUpperCase().endsWith("K")) {
1546 value = value.substring(0, value.length() - 1);
1548 frequency = Double.parseDouble(value);
1550 updateChannelState(CHANNEL_FREQUENCY);
1553 brightness = Integer.parseInt(value);
1554 updateChannelState(CHANNEL_BRIGHTNESS);
1556 case KEY_UPDATE_MODE:
1557 case KEY_DISPLAY_UPDATE:
1560 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1562 updateChannelState(CHANNEL_TCBYPASS);
1563 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1565 updateChannelState(CHANNEL_TCBYPASS);
1567 throw new RotelException("Invalid value");
1571 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1573 updateChannelState(CHANNEL_TCBYPASS);
1574 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1576 updateChannelState(CHANNEL_TCBYPASS);
1578 throw new RotelException("Invalid value");
1582 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1583 balance = minBalanceLevel;
1584 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1585 balance = maxBalanceLevel;
1586 } else if (value.toUpperCase().startsWith("L")) {
1587 balance = -Integer.parseInt(value.substring(1));
1588 } else if (value.toLowerCase().startsWith("R")) {
1589 balance = Integer.parseInt(value.substring(1));
1591 balance = Integer.parseInt(value);
1593 updateChannelState(CHANNEL_BALANCE);
1596 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1599 updateChannelState(CHANNEL_SPEAKER_A);
1600 updateChannelState(CHANNEL_SPEAKER_B);
1601 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1604 updateChannelState(CHANNEL_SPEAKER_A);
1605 updateChannelState(CHANNEL_SPEAKER_B);
1606 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1609 updateChannelState(CHANNEL_SPEAKER_A);
1610 updateChannelState(CHANNEL_SPEAKER_B);
1611 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1614 updateChannelState(CHANNEL_SPEAKER_A);
1615 updateChannelState(CHANNEL_SPEAKER_B);
1617 throw new RotelException("Invalid value");
1621 logger.debug("onNewMessageEvent: unhandled key {}", key);
1624 } catch (NumberFormatException | RotelException e) {
1625 logger.debug("Invalid value {} for key {}", value, key);
1630 * Handle the received information that device power (main zone) is ON
1632 private void handlePowerOn() {
1633 Boolean prev = power;
1635 updateChannelState(CHANNEL_POWER);
1636 updateChannelState(CHANNEL_MAIN_POWER);
1637 if ((prev == null) || !prev) {
1638 schedulePowerOnJob();
1643 * Handle the received information that device power (main zone) is OFF
1645 private void handlePowerOff() {
1648 updateChannelState(CHANNEL_POWER);
1649 updateChannelState(CHANNEL_MAIN_POWER);
1650 updateChannelState(CHANNEL_SOURCE);
1651 updateChannelState(CHANNEL_MAIN_SOURCE);
1652 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1653 updateChannelState(CHANNEL_DSP);
1654 updateChannelState(CHANNEL_MAIN_DSP);
1655 updateChannelState(CHANNEL_VOLUME);
1656 updateChannelState(CHANNEL_MAIN_VOLUME);
1657 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1658 updateChannelState(CHANNEL_MUTE);
1659 updateChannelState(CHANNEL_MAIN_MUTE);
1660 updateChannelState(CHANNEL_BASS);
1661 updateChannelState(CHANNEL_MAIN_BASS);
1662 updateChannelState(CHANNEL_TREBLE);
1663 updateChannelState(CHANNEL_MAIN_TREBLE);
1664 updateChannelState(CHANNEL_PLAY_CONTROL);
1665 updateChannelState(CHANNEL_TRACK);
1666 updateChannelState(CHANNEL_FREQUENCY);
1667 updateChannelState(CHANNEL_BRIGHTNESS);
1668 updateChannelState(CHANNEL_TCBYPASS);
1669 updateChannelState(CHANNEL_BALANCE);
1670 updateChannelState(CHANNEL_SPEAKER_A);
1671 updateChannelState(CHANNEL_SPEAKER_B);
1675 * Handle the received information that zone 2 power is ON
1677 private void handlePowerOnZone2() {
1678 boolean prev = powerZone2;
1680 updateChannelState(CHANNEL_ZONE2_POWER);
1682 schedulePowerOnZone2Job();
1687 * Handle the received information that zone 2 power is OFF
1689 private void handlePowerOffZone2() {
1690 cancelPowerOnZone2Job();
1692 updateChannelState(CHANNEL_ZONE2_POWER);
1693 updateChannelState(CHANNEL_ZONE2_SOURCE);
1694 updateChannelState(CHANNEL_ZONE2_VOLUME);
1695 updateChannelState(CHANNEL_ZONE2_VOLUME_UP_DOWN);
1696 updateChannelState(CHANNEL_ZONE2_MUTE);
1700 * Handle the received information that zone 3 power is ON
1702 private void handlePowerOnZone3() {
1703 boolean prev = powerZone3;
1705 updateChannelState(CHANNEL_ZONE3_POWER);
1707 schedulePowerOnZone3Job();
1712 * Handle the received information that zone 3 power is OFF
1714 private void handlePowerOffZone3() {
1715 cancelPowerOnZone3Job();
1717 updateChannelState(CHANNEL_ZONE3_POWER);
1718 updateChannelState(CHANNEL_ZONE3_SOURCE);
1719 updateChannelState(CHANNEL_ZONE3_VOLUME);
1720 updateChannelState(CHANNEL_ZONE3_MUTE);
1724 * Handle the received information that zone 4 power is ON
1726 private void handlePowerOnZone4() {
1727 boolean prev = powerZone4;
1729 updateChannelState(CHANNEL_ZONE4_POWER);
1731 schedulePowerOnZone4Job();
1736 * Handle the received information that zone 4 power is OFF
1738 private void handlePowerOffZone4() {
1739 cancelPowerOnZone4Job();
1741 updateChannelState(CHANNEL_ZONE4_POWER);
1742 updateChannelState(CHANNEL_ZONE4_SOURCE);
1743 updateChannelState(CHANNEL_ZONE4_VOLUME);
1744 updateChannelState(CHANNEL_ZONE4_MUTE);
1748 * Schedule the job that will consider the device as OFF if no new event is received before its running
1750 * @param switchOffAllZones true if all zones have to be considered as OFF
1752 private void schedulePowerOffJob(boolean switchOffAllZones) {
1753 logger.debug("Schedule power OFF job");
1754 cancelPowerOffJob();
1755 powerOffJob = scheduler.schedule(() -> {
1756 logger.debug("Power OFF job");
1758 if (switchOffAllZones) {
1759 handlePowerOffZone2();
1760 handlePowerOffZone3();
1761 handlePowerOffZone4();
1763 }, 2000, TimeUnit.MILLISECONDS);
1767 * Cancel the job that will consider the device as OFF
1769 private void cancelPowerOffJob() {
1770 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1771 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1772 powerOffJob.cancel(true);
1773 this.powerOffJob = null;
1778 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1780 private void schedulePowerOnJob() {
1781 logger.debug("Schedule power ON job");
1783 powerOnJob = scheduler.schedule(() -> {
1784 synchronized (sequenceLock) {
1785 logger.debug("Power ON job");
1789 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1790 sendCommand(getVolumeDownCommand());
1792 sendCommand(getVolumeUpCommand());
1795 if (model.getNbAdditionalZones() >= 1) {
1796 if (currentZone != 1
1797 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1798 selectZone(1, model.getZoneSelectCmd());
1799 } else if (!selectingRecord) {
1800 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1804 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1807 if (model.hasToneControl()) {
1808 if (model == RotelModel.RSX1065) {
1809 // No tone control select command
1810 sendCommand(RotelCommand.TREBLE_DOWN);
1812 sendCommand(RotelCommand.TREBLE_UP);
1814 sendCommand(RotelCommand.BASS_DOWN);
1816 sendCommand(RotelCommand.BASS_UP);
1819 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1824 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1825 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1826 sendCommand(RotelCommand.UPDATE_AUTO);
1827 Thread.sleep(SLEEP_INTV);
1829 if (model.hasSourceControl()) {
1830 sendCommand(RotelCommand.SOURCE);
1831 Thread.sleep(SLEEP_INTV);
1833 if (model.hasVolumeControl() || model.hasToneControl()) {
1834 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1835 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1836 sendCommand(RotelCommand.VOLUME_GET_MIN);
1837 Thread.sleep(SLEEP_INTV);
1838 sendCommand(RotelCommand.VOLUME_GET_MAX);
1839 Thread.sleep(SLEEP_INTV);
1841 if (model.hasToneControl()) {
1842 sendCommand(RotelCommand.TONE_MAX);
1843 Thread.sleep(SLEEP_INTV);
1845 // Wait enough to be sure to get the min/max values requested just before
1847 if (model.hasVolumeControl()) {
1848 sendCommand(RotelCommand.VOLUME_GET);
1849 Thread.sleep(SLEEP_INTV);
1850 if (model != RotelModel.RA11 && model != RotelModel.RA12
1851 && model != RotelModel.RCX1500) {
1852 sendCommand(RotelCommand.MUTE);
1853 Thread.sleep(SLEEP_INTV);
1856 if (model.hasToneControl()) {
1857 sendCommand(RotelCommand.BASS);
1858 Thread.sleep(SLEEP_INTV);
1859 sendCommand(RotelCommand.TREBLE);
1860 Thread.sleep(SLEEP_INTV);
1861 sendCommand(RotelCommand.TONE_CONTROLS);
1862 Thread.sleep(SLEEP_INTV);
1865 if (model.hasBalanceControl()) {
1866 sendCommand(RotelCommand.BALANCE);
1867 Thread.sleep(SLEEP_INTV);
1869 if (model.hasPlayControl()) {
1870 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1871 && (model != RotelModel.RCX1500 || !source.getName().equals("CD"))) {
1872 sendCommand(RotelCommand.PLAY_STATUS);
1873 Thread.sleep(SLEEP_INTV);
1875 sendCommand(RotelCommand.CD_PLAY_STATUS);
1876 Thread.sleep(SLEEP_INTV);
1879 if (model.hasDspControl()) {
1880 sendCommand(RotelCommand.DSP_MODE);
1881 Thread.sleep(SLEEP_INTV);
1883 if (model.canGetFrequency()) {
1884 sendCommand(RotelCommand.FREQUENCY);
1885 Thread.sleep(SLEEP_INTV);
1887 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1888 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1889 Thread.sleep(SLEEP_INTV);
1891 if (model.hasSpeakerGroups()) {
1892 sendCommand(RotelCommand.SPEAKER);
1893 Thread.sleep(SLEEP_INTV);
1897 sendCommand(RotelCommand.UPDATE_AUTO);
1898 Thread.sleep(SLEEP_INTV);
1899 if (model.hasSourceControl()) {
1900 sendCommand(RotelCommand.SOURCE);
1901 Thread.sleep(SLEEP_INTV);
1903 if (model.hasVolumeControl()) {
1904 sendCommand(RotelCommand.VOLUME_GET);
1905 Thread.sleep(SLEEP_INTV);
1906 sendCommand(RotelCommand.MUTE);
1907 Thread.sleep(SLEEP_INTV);
1909 if (model.hasToneControl()) {
1910 sendCommand(RotelCommand.BASS);
1911 Thread.sleep(SLEEP_INTV);
1912 sendCommand(RotelCommand.TREBLE);
1913 Thread.sleep(SLEEP_INTV);
1914 sendCommand(RotelCommand.TCBYPASS);
1915 Thread.sleep(SLEEP_INTV);
1917 if (model.hasBalanceControl()) {
1918 sendCommand(RotelCommand.BALANCE);
1919 Thread.sleep(SLEEP_INTV);
1921 if (model.hasPlayControl()) {
1922 sendCommand(RotelCommand.PLAY_STATUS);
1923 Thread.sleep(SLEEP_INTV);
1924 if (source.getName().equals("CD") && !model.hasSourceControl()) {
1925 sendCommand(RotelCommand.TRACK);
1926 Thread.sleep(SLEEP_INTV);
1929 if (model.hasDspControl()) {
1930 sendCommand(RotelCommand.DSP_MODE);
1931 Thread.sleep(SLEEP_INTV);
1933 if (model.canGetFrequency()) {
1934 sendCommand(RotelCommand.FREQUENCY);
1935 Thread.sleep(SLEEP_INTV);
1937 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1938 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1939 Thread.sleep(SLEEP_INTV);
1941 if (model.hasSpeakerGroups()) {
1942 sendCommand(RotelCommand.SPEAKER);
1943 Thread.sleep(SLEEP_INTV);
1947 } catch (RotelException e) {
1948 logger.debug("Init sequence failed: {}", e.getMessage());
1949 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1950 "@text/offline.comm-error-init-sequence");
1952 } catch (InterruptedException e) {
1953 logger.debug("Init sequence interrupted: {}", e.getMessage());
1954 Thread.currentThread().interrupt();
1957 }, 2500, TimeUnit.MILLISECONDS);
1961 * Cancel the job scheduled when the device power (main zone) switched ON
1963 private void cancelPowerOnJob() {
1964 ScheduledFuture<?> powerOnJob = this.powerOnJob;
1965 if (powerOnJob != null && !powerOnJob.isCancelled()) {
1966 powerOnJob.cancel(true);
1967 this.powerOnJob = null;
1972 * Schedule the job to run with a few seconds delay when the zone 2 power switched ON
1974 private void schedulePowerOnZone2Job() {
1975 logger.debug("Schedule power ON zone 2 job");
1976 cancelPowerOnZone2Job();
1977 powerOnZone2Job = scheduler.schedule(() -> {
1978 synchronized (sequenceLock) {
1979 logger.debug("Power ON zone 2 job");
1981 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 1) {
1982 selectZone(2, model.getZoneSelectCmd());
1984 model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
1986 sendCommand(model.hasZone2Commands() ? RotelCommand.ZONE2_VOLUME_UP : RotelCommand.VOLUME_UP);
1989 } catch (RotelException e) {
1990 logger.debug("Init sequence zone 2 failed: {}", e.getMessage());
1991 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1992 "@text/offline.comm-error-init-sequence-zone [\"2\"]");
1994 } catch (InterruptedException e) {
1995 logger.debug("Init sequence zone 2 interrupted: {}", e.getMessage());
1996 Thread.currentThread().interrupt();
1999 }, 2500, TimeUnit.MILLISECONDS);
2003 * Cancel the job scheduled when the zone 2 power switched ON
2005 private void cancelPowerOnZone2Job() {
2006 ScheduledFuture<?> powerOnZone2Job = this.powerOnZone2Job;
2007 if (powerOnZone2Job != null && !powerOnZone2Job.isCancelled()) {
2008 powerOnZone2Job.cancel(true);
2009 this.powerOnZone2Job = null;
2014 * Schedule the job to run with a few seconds delay when the zone 3 power switched ON
2016 private void schedulePowerOnZone3Job() {
2017 logger.debug("Schedule power ON zone 3 job");
2018 cancelPowerOnZone3Job();
2019 powerOnZone3Job = scheduler.schedule(() -> {
2020 synchronized (sequenceLock) {
2021 logger.debug("Power ON zone 3 job");
2023 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 2) {
2024 selectZone(3, model.getZoneSelectCmd());
2026 model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
2028 sendCommand(model.hasZone3Commands() ? RotelCommand.ZONE3_VOLUME_UP : RotelCommand.VOLUME_UP);
2031 } catch (RotelException e) {
2032 logger.debug("Init sequence zone 3 failed: {}", e.getMessage());
2033 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2034 "@text/offline.comm-error-init-sequence-zone [\"3\"]");
2036 } catch (InterruptedException e) {
2037 logger.debug("Init sequence zone 3 interrupted: {}", e.getMessage());
2038 Thread.currentThread().interrupt();
2041 }, 2500, TimeUnit.MILLISECONDS);
2045 * Cancel the job scheduled when the zone 3 power switched ON
2047 private void cancelPowerOnZone3Job() {
2048 ScheduledFuture<?> powerOnZone3Job = this.powerOnZone3Job;
2049 if (powerOnZone3Job != null && !powerOnZone3Job.isCancelled()) {
2050 powerOnZone3Job.cancel(true);
2051 this.powerOnZone3Job = null;
2056 * Schedule the job to run with a few seconds delay when the zone 4 power switched ON
2058 private void schedulePowerOnZone4Job() {
2059 logger.debug("Schedule power ON zone 4 job");
2060 cancelPowerOnZone4Job();
2061 powerOnZone4Job = scheduler.schedule(() -> {
2062 synchronized (sequenceLock) {
2063 logger.debug("Power ON zone 4 job");
2065 if (protocol == RotelProtocol.HEX && model.getNbAdditionalZones() >= 3) {
2066 selectZone(4, model.getZoneSelectCmd());
2068 model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_DOWN : RotelCommand.VOLUME_DOWN);
2070 sendCommand(model.hasZone4Commands() ? RotelCommand.ZONE4_VOLUME_UP : RotelCommand.VOLUME_UP);
2073 } catch (RotelException e) {
2074 logger.debug("Init sequence zone 4 failed: {}", e.getMessage());
2075 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2076 "@text/offline.comm-error-init-sequence-zone [\"4\"]");
2078 } catch (InterruptedException e) {
2079 logger.debug("Init sequence zone 4 interrupted: {}", e.getMessage());
2080 Thread.currentThread().interrupt();
2083 }, 2500, TimeUnit.MILLISECONDS);
2087 * Cancel the job scheduled when the zone 4 power switched ON
2089 private void cancelPowerOnZone4Job() {
2090 ScheduledFuture<?> powerOnZone4Job = this.powerOnZone4Job;
2091 if (powerOnZone4Job != null && !powerOnZone4Job.isCancelled()) {
2092 powerOnZone4Job.cancel(true);
2093 this.powerOnZone4Job = null;
2098 * Schedule the reconnection job
2100 private void scheduleReconnectJob() {
2101 logger.debug("Schedule reconnect job");
2102 cancelReconnectJob();
2103 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2104 if (!connector.isConnected()) {
2105 logger.debug("Trying to reconnect...");
2108 String error = null;
2109 if (openConnection()) {
2110 synchronized (sequenceLock) {
2111 schedulePowerOffJob(true);
2113 sendCommand(model.getPowerStateCmd());
2114 } catch (RotelException e) {
2115 error = "@text/offline.comm-error-first-command-after-reconnection";
2116 logger.debug("First command after connection failed", e);
2117 cancelPowerOffJob();
2122 error = "@text/offline.comm-error-reconnection";
2124 if (error != null) {
2126 handlePowerOffZone2();
2127 handlePowerOffZone3();
2128 handlePowerOffZone4();
2129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2131 updateStatus(ThingStatus.ONLINE);
2134 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2138 * Cancel the reconnection job
2140 private void cancelReconnectJob() {
2141 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2142 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2143 reconnectJob.cancel(true);
2144 this.reconnectJob = null;
2149 * Update the state of a channel
2151 * @param channel the channel
2153 private void updateChannelState(String channel) {
2154 if (!isLinked(channel)) {
2157 State state = UnDefType.UNDEF;
2160 case CHANNEL_MAIN_POWER:
2163 state = OnOffType.from(po.booleanValue());
2166 case CHANNEL_ZONE2_POWER:
2167 state = OnOffType.from(powerZone2);
2169 case CHANNEL_ZONE3_POWER:
2170 state = OnOffType.from(powerZone3);
2172 case CHANNEL_ZONE4_POWER:
2173 state = OnOffType.from(powerZone4);
2175 case CHANNEL_SOURCE:
2176 case CHANNEL_MAIN_SOURCE:
2178 state = new StringType(source.getName());
2181 case CHANNEL_MAIN_RECORD_SOURCE:
2182 RotelSource recordSource = this.recordSource;
2183 if (isPowerOn() && recordSource != null) {
2184 state = new StringType(recordSource.getName());
2187 case CHANNEL_ZONE2_SOURCE:
2188 RotelSource sourceZone2 = this.sourceZone2;
2189 if (powerZone2 && sourceZone2 != null) {
2190 state = new StringType(sourceZone2.getName());
2193 case CHANNEL_ZONE3_SOURCE:
2194 RotelSource sourceZone3 = this.sourceZone3;
2195 if (powerZone3 && sourceZone3 != null) {
2196 state = new StringType(sourceZone3.getName());
2199 case CHANNEL_ZONE4_SOURCE:
2200 RotelSource sourceZone4 = this.sourceZone4;
2201 if (powerZone4 && sourceZone4 != null) {
2202 state = new StringType(sourceZone4.getName());
2206 case CHANNEL_MAIN_DSP:
2208 state = new StringType(dsp.getName());
2211 case CHANNEL_VOLUME:
2212 case CHANNEL_MAIN_VOLUME:
2214 long volumePct = Math
2215 .round((double) (volume - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2216 state = new PercentType(BigDecimal.valueOf(volumePct));
2219 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2221 state = new DecimalType(volume);
2224 case CHANNEL_ZONE2_VOLUME:
2225 if (powerZone2 && !fixedVolumeZone2) {
2226 long volumePct = Math
2227 .round((double) (volumeZone2 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2228 state = new PercentType(BigDecimal.valueOf(volumePct));
2231 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2232 if (powerZone2 && !fixedVolumeZone2) {
2233 state = new DecimalType(volumeZone2);
2236 case CHANNEL_ZONE3_VOLUME:
2237 if (powerZone3 && !fixedVolumeZone3) {
2238 long volumePct = Math
2239 .round((double) (volumeZone3 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2240 state = new PercentType(BigDecimal.valueOf(volumePct));
2243 case CHANNEL_ZONE4_VOLUME:
2244 if (powerZone4 && !fixedVolumeZone4) {
2245 long volumePct = Math
2246 .round((double) (volumeZone4 - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2247 state = new PercentType(BigDecimal.valueOf(volumePct));
2251 case CHANNEL_MAIN_MUTE:
2253 state = OnOffType.from(mute);
2256 case CHANNEL_ZONE2_MUTE:
2258 state = OnOffType.from(muteZone2);
2261 case CHANNEL_ZONE3_MUTE:
2263 state = OnOffType.from(muteZone3);
2266 case CHANNEL_ZONE4_MUTE:
2268 state = OnOffType.from(muteZone4);
2272 case CHANNEL_MAIN_BASS:
2274 state = new DecimalType(bass);
2277 case CHANNEL_TREBLE:
2278 case CHANNEL_MAIN_TREBLE:
2280 state = new DecimalType(treble);
2284 if (track > 0 && isPowerOn()) {
2285 state = new DecimalType(track);
2288 case CHANNEL_PLAY_CONTROL:
2290 switch (playStatus) {
2292 state = PlayPauseType.PLAY;
2296 state = PlayPauseType.PAUSE;
2301 case CHANNEL_FREQUENCY:
2302 if (frequency > 0.0 && isPowerOn()) {
2303 state = new DecimalType(frequency);
2307 state = new StringType(frontPanelLine1);
2310 state = new StringType(frontPanelLine2);
2312 case CHANNEL_BRIGHTNESS:
2313 if (isPowerOn() && model.hasDimmerControl()) {
2314 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2315 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2316 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2319 case CHANNEL_TCBYPASS:
2321 state = OnOffType.from(tcbypass);
2324 case CHANNEL_BALANCE:
2326 state = new DecimalType(balance);
2329 case CHANNEL_SPEAKER_A:
2331 state = OnOffType.from(speakera);
2334 case CHANNEL_SPEAKER_B:
2336 state = OnOffType.from(speakerb);
2342 updateState(channel, state);
2346 * Inform about the main zone power state
2348 * @return true if main zone power state is known and known as ON
2350 private boolean isPowerOn() {
2351 Boolean power = this.power;
2352 return power != null && power.booleanValue();
2356 * Get the command to be used for main zone POWER ON
2358 * @return the command
2360 private RotelCommand getPowerOnCommand() {
2361 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2365 * Get the command to be used for main zone POWER OFF
2367 * @return the command
2369 private RotelCommand getPowerOffCommand() {
2370 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2374 * Get the command to be used for main zone VOLUME UP
2376 * @return the command
2378 private RotelCommand getVolumeUpCommand() {
2379 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2383 * Get the command to be used for main zone VOLUME DOWN
2385 * @return the command
2387 private RotelCommand getVolumeDownCommand() {
2388 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN : RotelCommand.VOLUME_DOWN;
2392 * Get the command to be used for main zone MUTE ON
2394 * @return the command
2396 private RotelCommand getMuteOnCommand() {
2397 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2401 * Get the command to be used for main zone MUTE OFF
2403 * @return the command
2405 private RotelCommand getMuteOffCommand() {
2406 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2410 * Get the command to be used for main zone MUTE TOGGLE
2412 * @return the command
2414 private RotelCommand getMuteToggleCommand() {
2415 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE : RotelCommand.MUTE_TOGGLE;
2418 private void sendCommand(RotelCommand cmd) throws RotelException {
2419 sendCommand(cmd, null);
2423 * Request the Rotel device to execute a command
2425 * @param cmd the command to execute
2426 * @param value the integer value to consider for volume, bass or treble adjustment
2428 * @throws RotelException - In case of any problem
2430 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2433 message = protocolHandler.buildCommandMessage(cmd, value);
2434 } catch (RotelException e) {
2435 // Command not supported
2436 logger.debug("sendCommand: {}", e.getMessage());
2439 connector.writeOutput(cmd.getName(), message);
2441 if (connector instanceof RotelSimuConnector) {
2442 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2443 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2444 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2445 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);