2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.rotel.internal.handler;
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.EventObject;
20 import java.util.HashMap;
21 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelBindingConstants;
29 import org.openhab.binding.rotel.internal.RotelCommandDescriptionOptionProvider;
30 import org.openhab.binding.rotel.internal.RotelException;
31 import org.openhab.binding.rotel.internal.RotelModel;
32 import org.openhab.binding.rotel.internal.RotelPlayStatus;
33 import org.openhab.binding.rotel.internal.RotelRepeatMode;
34 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
35 import org.openhab.binding.rotel.internal.communication.RotelCommand;
36 import org.openhab.binding.rotel.internal.communication.RotelConnector;
37 import org.openhab.binding.rotel.internal.communication.RotelDsp;
38 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
40 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
41 import org.openhab.binding.rotel.internal.communication.RotelSource;
42 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
43 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
44 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
45 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
46 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
47 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
48 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
49 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
50 import org.openhab.core.io.transport.serial.SerialPortManager;
51 import org.openhab.core.library.types.DecimalType;
52 import org.openhab.core.library.types.IncreaseDecreaseType;
53 import org.openhab.core.library.types.NextPreviousType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.PercentType;
56 import org.openhab.core.library.types.PlayPauseType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingStatus;
61 import org.openhab.core.thing.ThingStatusDetail;
62 import org.openhab.core.thing.binding.BaseThingHandler;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.CommandOption;
65 import org.openhab.core.types.RefreshType;
66 import org.openhab.core.types.State;
67 import org.openhab.core.types.StateOption;
68 import org.openhab.core.types.UnDefType;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
73 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
75 * @author Laurent Garnier - Initial contribution
78 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
80 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
82 private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
83 private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
84 private static final boolean USE_SIMULATED_DEVICE = false;
85 private static final int SLEEP_INTV = 30;
87 private final RotelStateDescriptionOptionProvider stateDescriptionProvider;
88 private final RotelCommandDescriptionOptionProvider commandDescriptionProvider;
89 private final SerialPortManager serialPortManager;
91 private @Nullable ScheduledFuture<?> reconnectJob;
92 private @Nullable ScheduledFuture<?> powerOffJob;
93 private @Nullable ScheduledFuture<?>[] powerOnZoneJobs = { null, null, null, null, null };
95 private RotelModel model;
96 private RotelProtocol protocol;
97 private RotelAbstractProtocolHandler protocolHandler;
98 private RotelConnector connector;
100 private int minVolume;
101 private int maxVolume;
102 private int minToneLevel;
103 private int maxToneLevel;
105 private int currentZone = 1;
106 private boolean selectingRecord;
107 private @Nullable Boolean[] powers = { null, false, false, false, false };
108 private boolean powerControlPerZone;
109 private @Nullable RotelSource recordSource;
110 private @Nullable RotelSource[] sources = { RotelSource.CAT0_CD, null, null, null, null };
111 private RotelDsp dsp = RotelDsp.CAT1_NONE;
112 private boolean[] fixedVolumeZones = { false, false, false, false, false };
113 private int[] volumes = { 0, 0, 0, 0, 0 };
114 private boolean[] mutes = { false, false, false, false, false };
115 private int[] basses = { 0, 0, 0, 0, 0 };
116 private int[] trebles = { 0, 0, 0, 0, 0 };
117 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
119 private boolean randomMode;
120 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
121 private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
122 private String frontPanelLine1 = "";
123 private String frontPanelLine2 = "";
124 private int brightness;
125 private boolean tcbypass;
126 private int[] balances = { 0, 0, 0, 0, 0 };
127 private int minBalanceLevel;
128 private int maxBalanceLevel;
129 private boolean speakera;
130 private boolean speakerb;
132 private Object sequenceLock = new Object();
137 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
138 RotelCommandDescriptionOptionProvider commandDescriptionProvider, SerialPortManager serialPortManager) {
140 this.stateDescriptionProvider = stateDescriptionProvider;
141 this.commandDescriptionProvider = commandDescriptionProvider;
142 this.serialPortManager = serialPortManager;
143 this.model = DEFAULT_MODEL;
144 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
145 this.protocol = protocolHandler.getProtocol();
146 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
150 public void initialize() {
151 logger.debug("Start initializing handler for thing {}", getThing().getUID());
153 switch (getThing().getThingTypeUID().getId()) {
154 case THING_TYPE_ID_RSP1066:
155 model = RotelModel.RSP1066;
157 case THING_TYPE_ID_RSP1068:
158 model = RotelModel.RSP1068;
160 case THING_TYPE_ID_RSP1069:
161 model = RotelModel.RSP1069;
163 case THING_TYPE_ID_RSP1098:
164 model = RotelModel.RSP1098;
166 case THING_TYPE_ID_RSP1570:
167 model = RotelModel.RSP1570;
169 case THING_TYPE_ID_RSP1572:
170 model = RotelModel.RSP1572;
172 case THING_TYPE_ID_RSX1055:
173 model = RotelModel.RSX1055;
175 case THING_TYPE_ID_RSX1056:
176 model = RotelModel.RSX1056;
178 case THING_TYPE_ID_RSX1057:
179 model = RotelModel.RSX1057;
181 case THING_TYPE_ID_RSX1058:
182 model = RotelModel.RSX1058;
184 case THING_TYPE_ID_RSX1065:
185 model = RotelModel.RSX1065;
187 case THING_TYPE_ID_RSX1067:
188 model = RotelModel.RSX1067;
190 case THING_TYPE_ID_RSX1550:
191 model = RotelModel.RSX1550;
193 case THING_TYPE_ID_RSX1560:
194 model = RotelModel.RSX1560;
196 case THING_TYPE_ID_RSX1562:
197 model = RotelModel.RSX1562;
199 case THING_TYPE_ID_A11:
200 model = RotelModel.A11;
202 case THING_TYPE_ID_A12:
203 model = RotelModel.A12;
205 case THING_TYPE_ID_A14:
206 model = RotelModel.A14;
208 case THING_TYPE_ID_CD11:
209 model = RotelModel.CD11;
211 case THING_TYPE_ID_CD14:
212 model = RotelModel.CD14;
214 case THING_TYPE_ID_RA11:
215 model = RotelModel.RA11;
217 case THING_TYPE_ID_RA12:
218 model = RotelModel.RA12;
220 case THING_TYPE_ID_RA1570:
221 model = RotelModel.RA1570;
223 case THING_TYPE_ID_RA1572:
224 model = RotelModel.RA1572;
226 case THING_TYPE_ID_RA1592:
227 model = RotelModel.RA1592;
229 case THING_TYPE_ID_RAP1580:
230 model = RotelModel.RAP1580;
232 case THING_TYPE_ID_RC1570:
233 model = RotelModel.RC1570;
235 case THING_TYPE_ID_RC1572:
236 model = RotelModel.RC1572;
238 case THING_TYPE_ID_RC1590:
239 model = RotelModel.RC1590;
241 case THING_TYPE_ID_RCD1570:
242 model = RotelModel.RCD1570;
244 case THING_TYPE_ID_RCD1572:
245 model = RotelModel.RCD1572;
247 case THING_TYPE_ID_RCX1500:
248 model = RotelModel.RCX1500;
250 case THING_TYPE_ID_RDD1580:
251 model = RotelModel.RDD1580;
253 case THING_TYPE_ID_RDG1520:
254 case THING_TYPE_ID_RT09:
255 model = RotelModel.RDG1520;
257 case THING_TYPE_ID_RSP1576:
258 model = RotelModel.RSP1576;
260 case THING_TYPE_ID_RSP1582:
261 model = RotelModel.RSP1582;
263 case THING_TYPE_ID_RT11:
264 model = RotelModel.RT11;
266 case THING_TYPE_ID_RT1570:
267 model = RotelModel.RT1570;
269 case THING_TYPE_ID_T11:
270 model = RotelModel.T11;
272 case THING_TYPE_ID_T14:
273 model = RotelModel.T14;
275 case THING_TYPE_ID_C8:
276 model = RotelModel.C8;
278 case THING_TYPE_ID_M8:
279 model = RotelModel.M8;
281 case THING_TYPE_ID_P5:
282 model = RotelModel.P5;
284 case THING_TYPE_ID_S5:
285 model = RotelModel.S5;
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 powerControlPerZone = model.hasPowerControlPerZone();
349 // Check configuration settings
350 String configError = null;
351 if ((config.serialPort == null || config.serialPort.isEmpty())
352 && (config.host == null || config.host.isEmpty())) {
353 configError = "@text/offline.config-error-unknown-serialport-and-host";
354 } else if (config.host == null || config.host.isEmpty()) {
355 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
356 configError = "@text/offline.config-error-invalid-serial-over-ip";
359 if (config.port == null) {
360 configError = "@text/offline.config-error-unknown-port";
361 } else if (config.port <= 0) {
362 configError = "@text/offline.config-error-invalid-port";
366 if (configError != null) {
367 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
369 for (RotelSource src : model.getSources()) {
370 // Consider custom input labels
372 switch (src.getName()) {
374 label = config.inputLabelCd;
377 label = config.inputLabelTuner;
380 label = config.inputLabelTape;
383 label = config.inputLabelPhono;
386 label = config.inputLabelVideo1;
389 label = config.inputLabelVideo2;
392 label = config.inputLabelVideo3;
395 label = config.inputLabelVideo4;
398 label = config.inputLabelVideo5;
401 label = config.inputLabelVideo6;
404 label = config.inputLabelUsb;
407 label = config.inputLabelMulti;
412 if (label != null && !label.isEmpty()) {
413 sourcesCustomLabels.put(src, label);
415 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
418 if (protocol == RotelProtocol.HEX) {
419 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
420 } else if (protocol == RotelProtocol.ASCII_V1) {
421 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
423 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
426 if (USE_SIMULATED_DEVICE) {
427 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
428 } else if (config.serialPort != null) {
429 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
430 protocolHandler, readerThreadName);
432 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
435 if (model.hasSourceControl()) {
436 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
437 getStateOptions(model.getSources(), sourcesCustomLabels));
438 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
439 getStateOptions(model.getSources(), sourcesCustomLabels));
440 stateDescriptionProvider.setStateOptions(
441 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
442 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
444 if (model.hasZoneSourceControl(1)) {
445 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
446 getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
448 if (model.hasZoneSourceControl(2)) {
449 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
450 getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
452 if (model.hasZoneSourceControl(3)) {
453 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
454 getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
456 if (model.hasZoneSourceControl(4)) {
457 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
458 getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
460 if (model.hasDspControl()) {
461 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
462 model.getDspStateOptions());
463 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
464 model.getDspStateOptions());
467 List<CommandOption> options = model.getOtherCommandsOptions(protocol);
468 if (!options.isEmpty()) {
469 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_OTHER_COMMAND),
471 commandDescriptionProvider
472 .setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_OTHER_COMMAND), options);
475 updateStatus(ThingStatus.UNKNOWN);
477 scheduleReconnectJob();
480 logger.debug("Finished initializing!");
484 public void dispose() {
485 logger.debug("Disposing handler for thing {}", getThing().getUID());
487 for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
488 cancelPowerOnZoneJob(zone);
490 cancelReconnectJob();
495 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
496 List<StateOption> options = new ArrayList<>();
497 for (RotelSource item : list) {
498 String label = sourcesLabels.get(item);
499 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
505 public void handleCommand(ChannelUID channelUID, Command command) {
506 String channel = channelUID.getId();
508 if (getThing().getStatus() != ThingStatus.ONLINE) {
509 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
513 if (command instanceof RefreshType) {
514 updateChannelState(channel);
518 if (!connector.isConnected()) {
519 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
525 case CHANNEL_ZONE1_SOURCE:
526 case CHANNEL_ZONE1_VOLUME:
527 case CHANNEL_ZONE1_MUTE:
528 case CHANNEL_ZONE1_BASS:
529 case CHANNEL_ZONE1_TREBLE:
530 case CHANNEL_ZONE1_BALANCE:
533 case CHANNEL_ZONE2_POWER:
534 case CHANNEL_ZONE2_SOURCE:
535 case CHANNEL_ZONE2_VOLUME:
536 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
537 case CHANNEL_ZONE2_MUTE:
538 case CHANNEL_ZONE2_BASS:
539 case CHANNEL_ZONE2_TREBLE:
540 case CHANNEL_ZONE2_BALANCE:
543 case CHANNEL_ZONE3_POWER:
544 case CHANNEL_ZONE3_SOURCE:
545 case CHANNEL_ZONE3_VOLUME:
546 case CHANNEL_ZONE3_MUTE:
547 case CHANNEL_ZONE3_BASS:
548 case CHANNEL_ZONE3_TREBLE:
549 case CHANNEL_ZONE3_BALANCE:
552 case CHANNEL_ZONE4_POWER:
553 case CHANNEL_ZONE4_SOURCE:
554 case CHANNEL_ZONE4_VOLUME:
555 case CHANNEL_ZONE4_MUTE:
556 case CHANNEL_ZONE4_BASS:
557 case CHANNEL_ZONE4_TREBLE:
558 case CHANNEL_ZONE4_BALANCE:
567 boolean success = true;
568 synchronized (sequenceLock) {
572 case CHANNEL_MAIN_POWER:
573 case CHANNEL_ZONE2_POWER:
574 case CHANNEL_ZONE3_POWER:
575 case CHANNEL_ZONE4_POWER:
576 if (numZone == 0 || model.hasZoneCommands(numZone)) {
577 handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
578 } else if (numZone == 2 && model.getNumberOfZones() == 2) {
579 if (isPowerOn() || isPowerOn(numZone)) {
580 selectZone(2, model.getZoneSelectCmd());
582 sendCommand(RotelCommand.ZONE_SELECT);
585 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
588 case CHANNEL_ALL_POWER:
589 handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
592 case CHANNEL_MAIN_SOURCE:
593 case CHANNEL_ZONE1_SOURCE:
594 case CHANNEL_ZONE2_SOURCE:
595 case CHANNEL_ZONE3_SOURCE:
596 case CHANNEL_ZONE4_SOURCE:
597 if (!isPowerOn(numZone)) {
599 logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
600 numZone == 0 ? "device" : "zone " + numZone);
601 } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
602 src = model.getSourceFromName(command.toString());
604 cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
606 cmd = src.getZoneCommand(numZone);
610 if (model.canGetFrequency()) {
611 // send <new-source> returns
612 // 1.) the selected <new-source>
613 // 2.) the used frequency
615 // at response-time the frequency has the value of <old-source>
616 // so we must wait a short moment to get the frequency of <new-source>
618 sendCommand(RotelCommand.FREQUENCY);
620 updateChannelState(CHANNEL_FREQUENCY);
624 logger.debug("Command {} from channel {} failed: undefined source command", command,
627 } else if (numZone == 2 && model.getNumberOfZones() > 1) {
628 src = model.getSourceFromName(command.toString());
629 cmd = src.getCommand();
631 selectZone(2, model.getZoneSelectCmd());
633 if (model.canGetFrequency()) {
634 // send <new-source> returns
635 // 1.) the selected <new-source>
636 // 2.) the used frequency
638 // at response-time the frequency has the value of <old-source>
639 // so we must wait a short moment to get the frequency of <new-source>
641 sendCommand(RotelCommand.FREQUENCY);
643 updateChannelState(CHANNEL_FREQUENCY);
647 logger.debug("Command {} from channel {} failed: undefined source command", command,
652 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
655 case CHANNEL_MAIN_RECORD_SOURCE:
658 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
659 } else if (model.hasOtherThanPrimaryCommands()) {
660 src = model.getSourceFromName(command.toString());
661 cmd = src.getRecordCommand();
666 logger.debug("Command {} from channel {} failed: undefined record source command",
670 src = model.getSourceFromName(command.toString());
671 cmd = src.getCommand();
673 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
678 logger.debug("Command {} from channel {} failed: undefined source command", command,
684 case CHANNEL_MAIN_DSP:
687 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
689 sendCommand(model.getCommandFromDspName(command.toString()));
693 case CHANNEL_MAIN_VOLUME:
694 case CHANNEL_MAIN_VOLUME_UP_DOWN:
695 case CHANNEL_ZONE1_VOLUME:
696 case CHANNEL_ZONE2_VOLUME:
697 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
698 case CHANNEL_ZONE3_VOLUME:
699 case CHANNEL_ZONE4_VOLUME:
700 if (!isPowerOn(numZone)) {
702 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
703 numZone == 0 ? "device" : "zone " + numZone);
704 } else if (fixedVolumeZones[numZone]) {
706 logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
707 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
708 handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
709 getVolumeDownCommand(numZone),
710 CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
711 || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
712 : getVolumeSetCommand(numZone));
713 } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
714 selectZone(2, model.getZoneSelectCmd());
715 handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
716 RotelCommand.VOLUME_DOWN,
717 CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
720 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
724 case CHANNEL_MAIN_MUTE:
725 case CHANNEL_ZONE1_MUTE:
726 case CHANNEL_ZONE2_MUTE:
727 case CHANNEL_ZONE3_MUTE:
728 case CHANNEL_ZONE4_MUTE:
729 if (!isPowerOn(numZone)) {
731 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
732 numZone == 0 ? "device" : "zone " + numZone);
733 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
734 handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
735 getMuteOnCommand(numZone), getMuteOffCommand(numZone),
736 getMuteToggleCommand(numZone));
739 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
743 case CHANNEL_MAIN_BASS:
744 case CHANNEL_ZONE1_BASS:
745 case CHANNEL_ZONE2_BASS:
746 case CHANNEL_ZONE3_BASS:
747 case CHANNEL_ZONE4_BASS:
748 if (!isPowerOn(numZone)) {
750 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
751 numZone == 0 ? "device" : "zone " + numZone);
752 } else if (tcbypass) {
754 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
756 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
757 handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
758 getBassDownCommand(numZone), getBassSetCommand(numZone));
761 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
765 case CHANNEL_MAIN_TREBLE:
766 case CHANNEL_ZONE1_TREBLE:
767 case CHANNEL_ZONE2_TREBLE:
768 case CHANNEL_ZONE3_TREBLE:
769 case CHANNEL_ZONE4_TREBLE:
770 if (!isPowerOn(numZone)) {
772 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
773 numZone == 0 ? "device" : "zone " + numZone);
774 } else if (tcbypass) {
776 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
778 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
779 handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
780 getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
783 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
786 case CHANNEL_PLAY_CONTROL:
789 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
790 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
791 sendCommand(RotelCommand.PLAY);
792 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
793 sendCommand(RotelCommand.PAUSE);
794 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
795 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
796 Thread.sleep(SLEEP_INTV);
797 sendCommand(RotelCommand.PLAY_STATUS);
799 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
800 sendCommand(RotelCommand.TRACK_FWD);
801 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
802 sendCommand(RotelCommand.TRACK_BACK);
805 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
811 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
812 } else if (command instanceof OnOffType) {
813 sendCommand(RotelCommand.RANDOM_TOGGLE);
816 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
822 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
824 RotelRepeatMode currentMode = repeatMode;
825 RotelRepeatMode mode = RotelRepeatMode.OFF;
827 mode = RotelRepeatMode.getFromName(command.toString());
828 if (mode == currentMode) {
830 logger.debug("Command {} from channel {} ignored: no change requested", command,
833 } catch (RotelException e) {
835 logger.debug("Command {} from channel {} failed: invalid command value", command,
839 // Toggle TRACK -> DISC -> OFF
840 sendCommand(RotelCommand.REPEAT_TOGGLE);
841 if ((mode == RotelRepeatMode.OFF && currentMode == RotelRepeatMode.TRACK)
842 || (mode == RotelRepeatMode.TRACK && currentMode == RotelRepeatMode.DISC)
843 || (mode == RotelRepeatMode.DISC && currentMode == RotelRepeatMode.OFF)) {
844 Thread.sleep(SLEEP_INTV);
845 sendCommand(RotelCommand.REPEAT_TOGGLE);
850 case CHANNEL_BRIGHTNESS:
851 case CHANNEL_ALL_BRIGHTNESS:
854 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
855 } else if (!model.hasDimmerControl()) {
857 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
858 } else if (command instanceof PercentType) {
859 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
860 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
861 + model.getDimmerLevelMin();
862 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
865 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
868 case CHANNEL_TCBYPASS:
871 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
872 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
874 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
876 handleTcbypassCmd(channel, command,
877 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
878 : RotelCommand.TCBYPASS_ON,
879 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
880 : RotelCommand.TCBYPASS_OFF);
883 case CHANNEL_BALANCE:
884 case CHANNEL_ZONE1_BALANCE:
885 case CHANNEL_ZONE2_BALANCE:
886 case CHANNEL_ZONE3_BALANCE:
887 case CHANNEL_ZONE4_BALANCE:
888 if (!isPowerOn(numZone)) {
890 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
891 numZone == 0 ? "device" : "zone " + numZone);
892 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
894 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
896 handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
897 getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
900 case CHANNEL_SPEAKER_A:
903 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
905 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
906 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
909 case CHANNEL_SPEAKER_B:
912 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
914 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
915 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
918 case CHANNEL_OTHER_COMMAND:
919 case CHANNEL_MAIN_OTHER_COMMAND:
922 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
925 cmd = RotelCommand.getFromName(command.toString());
926 } catch (RotelException e) {
928 logger.debug("Command {} from channel {} failed: undefined command", command, channel);
938 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
942 logger.debug("Command {} from channel {} succeeded", command, channel);
944 updateChannelState(channel);
946 } catch (RotelException e) {
947 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
948 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
949 "@text/offline.comm-error-sending-command");
951 scheduleReconnectJob();
952 } catch (InterruptedException e) {
953 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
954 Thread.currentThread().interrupt();
960 * Handle a power ON/OFF command
962 * @param channel the channel
963 * @param command the received channel command (OnOffType)
964 * @param onCmd the command to be sent to the device to power it ON
965 * @param offCmd the command to be sent to the device to power it OFF
967 * @throws RotelException in case of communication error with the device
969 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
970 throws RotelException {
971 if (command instanceof OnOffType && command == OnOffType.ON) {
973 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
976 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
981 * Handle a volume command
983 * @param current the current volume
984 * @param channel the channel
985 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
986 * @param upCmd the command to be sent to the device to increase the volume
987 * @param downCmd the command to be sent to the device to decrease the volume
988 * @param setCmd the command to be sent to the device to set the volume at a value
990 * @throws RotelException in case of communication error with the device
992 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
993 @Nullable RotelCommand setCmd) throws RotelException {
994 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
996 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
997 sendCommand(downCmd);
998 } else if (command instanceof DecimalType && setCmd == null) {
999 int value = ((DecimalType) command).intValue();
1000 if (value >= minVolume && value <= maxVolume) {
1001 if (value > current) {
1003 } else if (value < current) {
1004 sendCommand(downCmd);
1007 } else if (command instanceof PercentType && setCmd != null) {
1008 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
1010 sendCommand(setCmd, value);
1012 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1017 * Handle a mute command
1019 * @param onlyToggle true if only the toggle command must be used
1020 * @param channel the channel
1021 * @param command the received channel command (OnOffType)
1022 * @param onCmd the command to be sent to the device to mute
1023 * @param offCmd the command to be sent to the device to unmute
1024 * @param toggleCmd the command to be sent to the device to toggle the mute state
1026 * @throws RotelException in case of communication error with the device
1028 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1029 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1030 if (command instanceof OnOffType) {
1032 sendCommand(toggleCmd);
1033 } else if (command == OnOffType.ON) {
1035 } else if (command == OnOffType.OFF) {
1036 sendCommand(offCmd);
1039 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1044 * Handle a tone level adjustment command (bass or treble)
1046 * @param current the current tone level
1047 * @param channel the channel
1048 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1049 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1050 * @param upCmd the command to be sent to the device to increase the tone level
1051 * @param downCmd the command to be sent to the device to decrease the tone level
1052 * @param setCmd the command to be sent to the device to set the tone level at a value
1054 * @throws RotelException in case of communication error with the device
1055 * @throws InterruptedException in case of interruption during a thread sleep
1057 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1058 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1059 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1060 selectToneControl(nbSelect);
1062 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1063 selectToneControl(nbSelect);
1064 sendCommand(downCmd);
1065 } else if (command instanceof DecimalType) {
1066 int value = ((DecimalType) command).intValue();
1067 if (value >= minToneLevel && value <= maxToneLevel) {
1068 if (protocol != RotelProtocol.HEX) {
1069 sendCommand(setCmd, value);
1070 } else if (value > current) {
1071 selectToneControl(nbSelect);
1073 } else if (value < current) {
1074 selectToneControl(nbSelect);
1075 sendCommand(downCmd);
1079 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1084 * Handle a tcbypass command (only for ASCII protocol)
1086 * @param channel the channel
1087 * @param command the received channel command (OnOffType)
1088 * @param onCmd the command to be sent to the device to bypass_on
1089 * @param offCmd the command to be sent to the device to bypass_off
1091 * @throws RotelException in case of communication error with the device
1093 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1094 throws RotelException, InterruptedException {
1095 if (command instanceof OnOffType) {
1096 if (command == OnOffType.ON) {
1100 updateChannelState(CHANNEL_BASS);
1101 updateChannelState(CHANNEL_TREBLE);
1102 } else if (command == OnOffType.OFF) {
1103 sendCommand(offCmd);
1105 sendCommand(RotelCommand.BASS);
1107 sendCommand(RotelCommand.TREBLE);
1110 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1115 * Handle a speaker command
1117 * @param onlyToggle true if only the toggle command must be used
1118 * @param channel the channel
1119 * @param command the received channel command (OnOffType)
1120 * @param onCmd the command to be sent to the device to speaker_x_on
1121 * @param offCmd the command to be sent to the device to speaker_x_off
1122 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1124 * @throws RotelException in case of communication error with the device
1126 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1127 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1128 if (command instanceof OnOffType) {
1130 sendCommand(toggleCmd);
1131 } else if (command == OnOffType.ON) {
1133 } else if (command == OnOffType.OFF) {
1134 sendCommand(offCmd);
1137 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1142 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1144 * @param channel the channel
1145 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1146 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1147 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1148 * @param setCmd the command to be sent to the device to set the balance at a value
1150 * @throws RotelException in case of communication error with the device
1151 * @throws InterruptedException in case of interruption during a thread sleep
1153 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1154 RotelCommand setCmd) throws RotelException, InterruptedException {
1155 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1156 sendCommand(rightCmd);
1157 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1158 sendCommand(leftCmd);
1159 } else if (command instanceof DecimalType) {
1160 int value = ((DecimalType) command).intValue();
1161 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1162 sendCommand(setCmd, value);
1165 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1170 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1172 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1174 * @throws RotelException in case of communication error with the device
1175 * @throws InterruptedException in case of interruption during a thread sleep
1177 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1178 // No tone control select command for RSX-1065
1179 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1180 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1185 * Run a sequence of commands to display a particular zone on the device front panel
1187 * @param zone the zone to be displayed (1 for main zone)
1188 * @param selectCommand the command to be sent to the device to switch the display between zones
1190 * @throws RotelException in case of communication error with the device
1191 * @throws InterruptedException in case of interruption during a thread sleep
1193 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1194 throws RotelException, InterruptedException {
1195 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
1196 && selectCommand != null) {
1198 if (zone < currentZone) {
1199 nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
1200 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1204 nbSelect = zone - currentZone;
1205 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1206 && !selectingRecord) {
1210 selectFeature(nbSelect, null, selectCommand);
1215 * Run a sequence of commands to display a particular feature on the device front panel
1217 * @param nbSelect the number of select commands to be run
1218 * @param preCmd the initial command to be sent to the device (before the select commands)
1219 * @param selectCmd the select command to be sent to the device
1221 * @throws RotelException in case of communication error with the device
1222 * @throws InterruptedException in case of interruption during a thread sleep
1224 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1225 throws RotelException, InterruptedException {
1226 if (protocol == RotelProtocol.HEX) {
1227 if (preCmd != null) {
1228 sendCommand(preCmd);
1231 for (int i = 1; i <= nbSelect; i++) {
1232 sendCommand(selectCmd);
1239 * Open the connection with the Rotel device
1241 * @return true if the connection is opened successfully or flase if not
1243 private synchronized boolean openConnection() {
1244 protocolHandler.addEventListener(this);
1247 } catch (RotelException e) {
1248 logger.debug("openConnection() failed", e);
1250 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1251 return connector.isConnected();
1255 * Close the connection with the Rotel device
1257 private synchronized void closeConnection() {
1259 protocolHandler.removeEventListener(this);
1260 logger.debug("closeConnection(): disconnected");
1264 public void onNewMessageEvent(EventObject event) {
1265 cancelPowerOffJob();
1267 RotelMessageEvent evt = (RotelMessageEvent) event;
1268 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1270 String key = evt.getKey();
1271 String value = evt.getValue().trim();
1272 if (!KEY_ERROR.equals(key)) {
1273 updateStatus(ThingStatus.ONLINE);
1277 case KEY_INPUT_ZONE1:
1278 case KEY_VOLUME_ZONE1:
1279 case KEY_MUTE_ZONE1:
1280 case KEY_BASS_ZONE1:
1281 case KEY_TREBLE_ZONE1:
1282 case KEY_BALANCE_ZONE1:
1283 case KEY_FREQ_ZONE1:
1286 case KEY_POWER_ZONE2:
1287 case KEY_SOURCE_ZONE2:
1288 case KEY_INPUT_ZONE2:
1289 case KEY_VOLUME_ZONE2:
1290 case KEY_MUTE_ZONE2:
1291 case KEY_BASS_ZONE2:
1292 case KEY_TREBLE_ZONE2:
1293 case KEY_BALANCE_ZONE2:
1294 case KEY_FREQ_ZONE2:
1297 case KEY_POWER_ZONE3:
1298 case KEY_SOURCE_ZONE3:
1299 case KEY_INPUT_ZONE3:
1300 case KEY_VOLUME_ZONE3:
1301 case KEY_MUTE_ZONE3:
1302 case KEY_BASS_ZONE3:
1303 case KEY_TREBLE_ZONE3:
1304 case KEY_BALANCE_ZONE3:
1305 case KEY_FREQ_ZONE3:
1308 case KEY_POWER_ZONE4:
1309 case KEY_SOURCE_ZONE4:
1310 case KEY_INPUT_ZONE4:
1311 case KEY_VOLUME_ZONE4:
1312 case KEY_MUTE_ZONE4:
1313 case KEY_BASS_ZONE4:
1314 case KEY_TREBLE_ZONE4:
1315 case KEY_BALANCE_ZONE4:
1316 case KEY_FREQ_ZONE4:
1325 logger.debug("Reading feedback message failed");
1326 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1327 "@text/offline.comm-error-reading-thread");
1331 frontPanelLine1 = value;
1332 updateChannelState(CHANNEL_LINE1);
1335 frontPanelLine2 = value;
1336 updateChannelState(CHANNEL_LINE2);
1339 currentZone = Integer.parseInt(value);
1341 case KEY_RECORD_SEL:
1342 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1345 if (POWER_ON.equalsIgnoreCase(value)) {
1347 } else if (STANDBY.equalsIgnoreCase(value)) {
1349 if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
1350 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1351 handlePowerOffZone(zone);
1354 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1355 schedulePowerOffJob(false);
1357 throw new RotelException("Invalid value");
1360 case KEY_POWER_ZONE2:
1361 case KEY_POWER_ZONE3:
1362 case KEY_POWER_ZONE4:
1363 if (POWER_ON.equalsIgnoreCase(value)) {
1364 handlePowerOnZone(numZone);
1365 } else if (STANDBY.equalsIgnoreCase(value)) {
1366 handlePowerOffZone(numZone);
1368 throw new RotelException("Invalid value");
1371 case KEY_VOLUME_MIN:
1372 minVolume = Integer.parseInt(value);
1373 if (!model.hasDirectVolumeControl()) {
1374 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1378 case KEY_VOLUME_MAX:
1379 maxVolume = Integer.parseInt(value);
1380 if (!model.hasDirectVolumeControl()) {
1381 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1386 case KEY_VOLUME_ZONE1:
1387 case KEY_VOLUME_ZONE2:
1388 case KEY_VOLUME_ZONE3:
1389 case KEY_VOLUME_ZONE4:
1390 fixedVolumeZones[numZone] = false;
1391 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1392 fixedVolumeZones[numZone] = true;
1393 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1394 volumes[numZone] = minVolume;
1395 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1396 volumes[numZone] = maxVolume;
1398 volumes[numZone] = Integer.parseInt(value);
1401 updateChannelState(CHANNEL_VOLUME);
1402 updateChannelState(CHANNEL_MAIN_VOLUME);
1403 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1405 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1406 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1410 case KEY_MUTE_ZONE1:
1411 case KEY_MUTE_ZONE2:
1412 case KEY_MUTE_ZONE3:
1413 case KEY_MUTE_ZONE4:
1414 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1415 mutes[numZone] = true;
1417 updateChannelState(CHANNEL_MUTE);
1418 updateChannelState(CHANNEL_MAIN_MUTE);
1420 updateGroupChannelState(numZone, CHANNEL_MUTE);
1422 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1423 mutes[numZone] = false;
1425 updateChannelState(CHANNEL_MUTE);
1426 updateChannelState(CHANNEL_MAIN_MUTE);
1428 updateGroupChannelState(numZone, CHANNEL_MUTE);
1431 throw new RotelException("Invalid value");
1435 maxToneLevel = Integer.parseInt(value);
1436 minToneLevel = -maxToneLevel;
1438 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1439 minToneLevel, maxToneLevel);
1442 case KEY_BASS_ZONE1:
1443 case KEY_BASS_ZONE2:
1444 case KEY_BASS_ZONE3:
1445 case KEY_BASS_ZONE4:
1446 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1447 basses[numZone] = minToneLevel;
1448 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1449 basses[numZone] = maxToneLevel;
1451 basses[numZone] = Integer.parseInt(value);
1454 updateChannelState(CHANNEL_BASS);
1455 updateChannelState(CHANNEL_MAIN_BASS);
1457 updateGroupChannelState(numZone, CHANNEL_BASS);
1461 case KEY_TREBLE_ZONE1:
1462 case KEY_TREBLE_ZONE2:
1463 case KEY_TREBLE_ZONE3:
1464 case KEY_TREBLE_ZONE4:
1465 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1466 trebles[numZone] = minToneLevel;
1467 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1468 trebles[numZone] = maxToneLevel;
1470 trebles[numZone] = Integer.parseInt(value);
1473 updateChannelState(CHANNEL_TREBLE);
1474 updateChannelState(CHANNEL_MAIN_TREBLE);
1476 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1480 sources[0] = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1481 updateChannelState(CHANNEL_SOURCE);
1482 updateChannelState(CHANNEL_MAIN_SOURCE);
1485 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1486 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1488 case KEY_SOURCE_ZONE2:
1489 case KEY_SOURCE_ZONE3:
1490 case KEY_SOURCE_ZONE4:
1491 case KEY_INPUT_ZONE1:
1492 case KEY_INPUT_ZONE2:
1493 case KEY_INPUT_ZONE3:
1494 case KEY_INPUT_ZONE4:
1495 sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
1496 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1499 if ("dolby_pliix_movie".equals(value)) {
1500 value = "dolby_plii_movie";
1501 } else if ("dolby_pliix_music".equals(value)) {
1502 value = "dolby_plii_music";
1503 } else if ("dolby_pliix_game".equals(value)) {
1504 value = "dolby_plii_game";
1506 dsp = model.getDspFromFeedback(value);
1507 logger.debug("DSP {}", dsp.getName());
1508 updateChannelState(CHANNEL_DSP);
1509 updateChannelState(CHANNEL_MAIN_DSP);
1511 case KEY1_PLAY_STATUS:
1512 case KEY2_PLAY_STATUS:
1513 if (PLAY.equalsIgnoreCase(value)) {
1514 playStatus = RotelPlayStatus.PLAYING;
1515 updateChannelState(CHANNEL_PLAY_CONTROL);
1516 } else if (PAUSE.equalsIgnoreCase(value)) {
1517 playStatus = RotelPlayStatus.PAUSED;
1518 updateChannelState(CHANNEL_PLAY_CONTROL);
1519 } else if (STOP.equalsIgnoreCase(value)) {
1520 playStatus = RotelPlayStatus.STOPPED;
1521 updateChannelState(CHANNEL_PLAY_CONTROL);
1523 throw new RotelException("Invalid value");
1527 RotelSource source = sources[0];
1528 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1529 track = Integer.parseInt(value);
1530 updateChannelState(CHANNEL_TRACK);
1534 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1536 updateChannelState(CHANNEL_RANDOM);
1537 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1539 updateChannelState(CHANNEL_RANDOM);
1541 throw new RotelException("Invalid value");
1545 if (TRACK.equalsIgnoreCase(value)) {
1546 repeatMode = RotelRepeatMode.TRACK;
1547 updateChannelState(CHANNEL_REPEAT);
1548 } else if (DISC.equalsIgnoreCase(value)) {
1549 repeatMode = RotelRepeatMode.DISC;
1550 updateChannelState(CHANNEL_REPEAT);
1551 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1552 repeatMode = RotelRepeatMode.OFF;
1553 updateChannelState(CHANNEL_REPEAT);
1555 throw new RotelException("Invalid value");
1559 case KEY_FREQ_ZONE1:
1560 case KEY_FREQ_ZONE2:
1561 case KEY_FREQ_ZONE3:
1562 case KEY_FREQ_ZONE4:
1563 if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
1564 frequencies[numZone] = 0.0;
1566 // Suppress a potential ending "k" or "K"
1567 if (value.toUpperCase().endsWith("K")) {
1568 value = value.substring(0, value.length() - 1);
1570 frequencies[numZone] = Double.parseDouble(value);
1573 updateChannelState(CHANNEL_FREQUENCY);
1575 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1579 brightness = Integer.parseInt(value);
1580 updateChannelState(CHANNEL_BRIGHTNESS);
1581 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1583 case KEY_UPDATE_MODE:
1584 case KEY_DISPLAY_UPDATE:
1587 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1589 updateChannelState(CHANNEL_TCBYPASS);
1590 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1592 updateChannelState(CHANNEL_TCBYPASS);
1594 throw new RotelException("Invalid value");
1598 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1600 updateChannelState(CHANNEL_TCBYPASS);
1601 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1603 updateChannelState(CHANNEL_TCBYPASS);
1605 throw new RotelException("Invalid value");
1609 case KEY_BALANCE_ZONE1:
1610 case KEY_BALANCE_ZONE2:
1611 case KEY_BALANCE_ZONE3:
1612 case KEY_BALANCE_ZONE4:
1613 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1614 balances[numZone] = minBalanceLevel;
1615 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1616 balances[numZone] = maxBalanceLevel;
1617 } else if (value.toUpperCase().startsWith("L")) {
1618 balances[numZone] = -Integer.parseInt(value.substring(1));
1619 } else if (value.toUpperCase().startsWith("R")) {
1620 balances[numZone] = Integer.parseInt(value.substring(1));
1622 balances[numZone] = Integer.parseInt(value);
1625 updateChannelState(CHANNEL_BALANCE);
1627 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1631 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1634 updateChannelState(CHANNEL_SPEAKER_A);
1635 updateChannelState(CHANNEL_SPEAKER_B);
1636 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1639 updateChannelState(CHANNEL_SPEAKER_A);
1640 updateChannelState(CHANNEL_SPEAKER_B);
1641 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1644 updateChannelState(CHANNEL_SPEAKER_A);
1645 updateChannelState(CHANNEL_SPEAKER_B);
1646 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1649 updateChannelState(CHANNEL_SPEAKER_A);
1650 updateChannelState(CHANNEL_SPEAKER_B);
1652 throw new RotelException("Invalid value");
1656 getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
1659 getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
1662 logger.debug("onNewMessageEvent: unhandled key {}", key);
1665 } catch (NumberFormatException | RotelException e) {
1666 logger.debug("Invalid value {} for key {}", value, key);
1671 * Handle the received information that device power (main zone) is ON
1673 private void handlePowerOn() {
1674 Boolean prev = powers[0];
1676 updateChannelState(CHANNEL_POWER);
1677 updateChannelState(CHANNEL_MAIN_POWER);
1678 updateChannelState(CHANNEL_ALL_POWER);
1679 if ((prev == null) || !prev) {
1680 schedulePowerOnJob();
1685 * Handle the received information that device power (main zone) is OFF
1687 private void handlePowerOff() {
1688 cancelPowerOnZoneJob(0);
1690 updateChannelState(CHANNEL_POWER);
1691 updateChannelState(CHANNEL_SOURCE);
1692 updateChannelState(CHANNEL_DSP);
1693 updateChannelState(CHANNEL_VOLUME);
1694 updateChannelState(CHANNEL_MUTE);
1695 updateChannelState(CHANNEL_BASS);
1696 updateChannelState(CHANNEL_TREBLE);
1697 updateChannelState(CHANNEL_PLAY_CONTROL);
1698 updateChannelState(CHANNEL_TRACK);
1699 updateChannelState(CHANNEL_RANDOM);
1700 updateChannelState(CHANNEL_REPEAT);
1701 updateChannelState(CHANNEL_FREQUENCY);
1702 updateChannelState(CHANNEL_BRIGHTNESS);
1703 updateChannelState(CHANNEL_TCBYPASS);
1704 updateChannelState(CHANNEL_BALANCE);
1705 updateChannelState(CHANNEL_SPEAKER_A);
1706 updateChannelState(CHANNEL_SPEAKER_B);
1708 updateChannelState(CHANNEL_MAIN_POWER);
1709 updateChannelState(CHANNEL_MAIN_SOURCE);
1710 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1711 updateChannelState(CHANNEL_MAIN_DSP);
1712 updateChannelState(CHANNEL_MAIN_VOLUME);
1713 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1714 updateChannelState(CHANNEL_MAIN_MUTE);
1715 updateChannelState(CHANNEL_MAIN_BASS);
1716 updateChannelState(CHANNEL_MAIN_TREBLE);
1718 updateChannelState(CHANNEL_ALL_POWER);
1719 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1723 * Handle the received information that a zone power is ON
1725 private void handlePowerOnZone(int numZone) {
1726 Boolean prev = powers[numZone];
1727 powers[numZone] = true;
1728 updateGroupChannelState(numZone, CHANNEL_POWER);
1729 if ((prev == null) || !prev) {
1730 schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
1735 * Handle the received information that a zone power is OFF
1737 private void handlePowerOffZone(int numZone) {
1738 cancelPowerOnZoneJob(numZone);
1739 powers[numZone] = false;
1740 updateGroupChannelState(numZone, CHANNEL_POWER);
1741 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1742 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1743 updateGroupChannelState(numZone, CHANNEL_MUTE);
1744 updateGroupChannelState(numZone, CHANNEL_BASS);
1745 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1746 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1747 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1748 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1752 * Schedule the job that will consider the device as OFF if no new event is received before its running
1754 * @param switchOffAllZones true if all zones have to be considered as OFF
1756 private void schedulePowerOffJob(boolean switchOffAllZones) {
1757 logger.debug("Schedule power OFF job");
1758 cancelPowerOffJob();
1759 powerOffJob = scheduler.schedule(() -> {
1760 logger.debug("Power OFF job");
1762 if (switchOffAllZones) {
1763 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1764 handlePowerOffZone(zone);
1767 }, 2000, TimeUnit.MILLISECONDS);
1771 * Cancel the job that will consider the device as OFF
1773 private void cancelPowerOffJob() {
1774 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1775 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1776 powerOffJob.cancel(true);
1777 this.powerOffJob = null;
1782 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1784 private void schedulePowerOnJob() {
1785 logger.debug("Schedule power ON job");
1786 cancelPowerOnZoneJob(0);
1787 powerOnZoneJobs[0] = scheduler.schedule(() -> {
1788 synchronized (sequenceLock) {
1789 logger.debug("Power ON job");
1793 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1794 sendCommand(getVolumeDownCommand(0));
1796 sendCommand(getVolumeUpCommand(0));
1799 if (model.getNumberOfZones() > 1) {
1800 if (currentZone != 1
1801 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1802 selectZone(1, model.getZoneSelectCmd());
1803 } else if (!selectingRecord) {
1804 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1808 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1811 if (model.hasToneControl()) {
1812 if (model == RotelModel.RSX1065) {
1813 // No tone control select command
1814 sendCommand(RotelCommand.TREBLE_DOWN);
1816 sendCommand(RotelCommand.TREBLE_UP);
1818 sendCommand(RotelCommand.BASS_DOWN);
1820 sendCommand(RotelCommand.BASS_UP);
1823 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1828 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1829 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1830 sendCommand(RotelCommand.UPDATE_AUTO);
1831 Thread.sleep(SLEEP_INTV);
1833 if (model.hasSourceControl()) {
1834 sendCommand(RotelCommand.SOURCE);
1835 Thread.sleep(SLEEP_INTV);
1837 if (model.hasVolumeControl() || model.hasToneControl()) {
1838 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1839 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1840 sendCommand(RotelCommand.VOLUME_GET_MIN);
1841 Thread.sleep(SLEEP_INTV);
1842 sendCommand(RotelCommand.VOLUME_GET_MAX);
1843 Thread.sleep(SLEEP_INTV);
1845 if (model.hasToneControl()) {
1846 sendCommand(RotelCommand.TONE_MAX);
1847 Thread.sleep(SLEEP_INTV);
1849 // Wait enough to be sure to get the min/max values requested just before
1851 if (model.hasVolumeControl()) {
1852 sendCommand(RotelCommand.VOLUME_GET);
1853 Thread.sleep(SLEEP_INTV);
1854 if (model != RotelModel.RA11 && model != RotelModel.RA12
1855 && model != RotelModel.RCX1500) {
1856 sendCommand(RotelCommand.MUTE);
1857 Thread.sleep(SLEEP_INTV);
1860 if (model.hasToneControl()) {
1861 sendCommand(RotelCommand.BASS);
1862 Thread.sleep(SLEEP_INTV);
1863 sendCommand(RotelCommand.TREBLE);
1864 Thread.sleep(SLEEP_INTV);
1865 if (model.canGetBypassStatus()) {
1866 sendCommand(RotelCommand.TONE_CONTROLS);
1867 Thread.sleep(SLEEP_INTV);
1871 if (model.hasBalanceControl()) {
1872 sendCommand(RotelCommand.BALANCE);
1873 Thread.sleep(SLEEP_INTV);
1875 if (model.hasPlayControl()) {
1876 RotelSource source = sources[0];
1877 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1878 && (model != RotelModel.RCX1500 || source == null
1879 || !source.getName().equals("CD"))) {
1880 sendCommand(RotelCommand.PLAY_STATUS);
1881 Thread.sleep(SLEEP_INTV);
1883 sendCommand(RotelCommand.CD_PLAY_STATUS);
1884 Thread.sleep(SLEEP_INTV);
1887 if (model.hasDspControl()) {
1888 sendCommand(RotelCommand.DSP_MODE);
1889 Thread.sleep(SLEEP_INTV);
1891 if (model.canGetFrequency()) {
1892 sendCommand(RotelCommand.FREQUENCY);
1893 Thread.sleep(SLEEP_INTV);
1895 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1896 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1897 Thread.sleep(SLEEP_INTV);
1899 if (model.hasSpeakerGroups()) {
1900 sendCommand(RotelCommand.SPEAKER);
1901 Thread.sleep(SLEEP_INTV);
1905 sendCommand(RotelCommand.UPDATE_AUTO);
1906 Thread.sleep(SLEEP_INTV);
1907 if (model.hasSourceControl()) {
1908 if (model.getNumberOfZones() > 1) {
1909 sendCommand(RotelCommand.INPUT);
1911 sendCommand(RotelCommand.SOURCE);
1913 Thread.sleep(SLEEP_INTV);
1915 if (model.hasVolumeControl()) {
1916 sendCommand(RotelCommand.VOLUME_GET);
1917 Thread.sleep(SLEEP_INTV);
1918 sendCommand(RotelCommand.MUTE);
1919 Thread.sleep(SLEEP_INTV);
1921 if (model.hasToneControl()) {
1922 sendCommand(RotelCommand.BASS);
1923 Thread.sleep(SLEEP_INTV);
1924 sendCommand(RotelCommand.TREBLE);
1925 Thread.sleep(SLEEP_INTV);
1926 if (model.canGetBypassStatus()) {
1927 sendCommand(RotelCommand.TCBYPASS);
1928 Thread.sleep(SLEEP_INTV);
1931 if (model.hasBalanceControl()) {
1932 sendCommand(RotelCommand.BALANCE);
1933 Thread.sleep(SLEEP_INTV);
1935 if (model.hasPlayControl()) {
1936 sendCommand(RotelCommand.PLAY_STATUS);
1937 Thread.sleep(SLEEP_INTV);
1938 RotelSource source = sources[0];
1939 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1940 sendCommand(RotelCommand.TRACK);
1941 Thread.sleep(SLEEP_INTV);
1942 sendCommand(RotelCommand.RANDOM_MODE);
1943 Thread.sleep(SLEEP_INTV);
1944 sendCommand(RotelCommand.REPEAT_MODE);
1945 Thread.sleep(SLEEP_INTV);
1948 if (model.hasDspControl()) {
1949 sendCommand(RotelCommand.DSP_MODE);
1950 Thread.sleep(SLEEP_INTV);
1952 if (model.canGetFrequency()) {
1953 sendCommand(RotelCommand.FREQUENCY);
1954 Thread.sleep(SLEEP_INTV);
1956 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1957 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1958 Thread.sleep(SLEEP_INTV);
1960 if (model.hasSpeakerGroups()) {
1961 sendCommand(RotelCommand.SPEAKER);
1962 Thread.sleep(SLEEP_INTV);
1964 sendCommand(RotelCommand.MODEL);
1965 Thread.sleep(SLEEP_INTV);
1966 sendCommand(RotelCommand.VERSION);
1967 Thread.sleep(SLEEP_INTV);
1970 } catch (RotelException e) {
1971 logger.debug("Init sequence failed: {}", e.getMessage());
1972 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1973 "@text/offline.comm-error-init-sequence");
1975 } catch (InterruptedException e) {
1976 logger.debug("Init sequence interrupted: {}", e.getMessage());
1977 Thread.currentThread().interrupt();
1980 }, 2500, TimeUnit.MILLISECONDS);
1984 * Schedule the job to run with a few seconds delay when the zone power switched ON
1986 private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
1987 logger.debug("Schedule power ON zone {} job", numZone);
1988 cancelPowerOnZoneJob(numZone);
1989 powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
1990 synchronized (sequenceLock) {
1991 logger.debug("Power ON zone {} job", numZone);
1993 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
1994 selectZone(numZone, model.getZoneSelectCmd());
1995 sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
1997 sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
2000 } catch (RotelException e) {
2001 logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
2002 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
2003 String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
2005 } catch (InterruptedException e) {
2006 logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
2007 Thread.currentThread().interrupt();
2010 }, 2500, TimeUnit.MILLISECONDS);
2014 * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
2016 private void cancelPowerOnZoneJob(int numZone) {
2017 ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
2018 if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
2019 powerOnZoneJob.cancel(true);
2020 powerOnZoneJobs[numZone] = null;
2025 * Schedule the reconnection job
2027 private void scheduleReconnectJob() {
2028 logger.debug("Schedule reconnect job");
2029 cancelReconnectJob();
2030 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2031 if (!connector.isConnected()) {
2032 logger.debug("Trying to reconnect...");
2035 String error = null;
2036 if (openConnection()) {
2037 synchronized (sequenceLock) {
2038 schedulePowerOffJob(true);
2040 sendCommand(model.getPowerStateCmd());
2041 } catch (RotelException e) {
2042 error = "@text/offline.comm-error-first-command-after-reconnection";
2043 logger.debug("First command after connection failed", e);
2044 cancelPowerOffJob();
2049 error = "@text/offline.comm-error-reconnection";
2051 if (error != null) {
2053 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
2054 handlePowerOffZone(zone);
2056 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2058 updateStatus(ThingStatus.ONLINE);
2061 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2065 * Cancel the reconnection job
2067 private void cancelReconnectJob() {
2068 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2069 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2070 reconnectJob.cancel(true);
2071 this.reconnectJob = null;
2075 private void updateGroupChannelState(int numZone, String channel) {
2076 updateChannelState(String.format("zone%d#%s", numZone, channel));
2080 * Update the state of a channel
2082 * @param channel the channel
2084 private void updateChannelState(String channel) {
2085 if (!isLinked(channel)) {
2088 State state = UnDefType.UNDEF;
2089 RotelSource localSource;
2092 case CHANNEL_ZONE1_SOURCE:
2093 case CHANNEL_ZONE1_VOLUME:
2094 case CHANNEL_ZONE1_MUTE:
2095 case CHANNEL_ZONE1_BASS:
2096 case CHANNEL_ZONE1_TREBLE:
2097 case CHANNEL_ZONE1_BALANCE:
2098 case CHANNEL_ZONE1_FREQUENCY:
2101 case CHANNEL_ZONE2_POWER:
2102 case CHANNEL_ZONE2_SOURCE:
2103 case CHANNEL_ZONE2_VOLUME:
2104 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2105 case CHANNEL_ZONE2_MUTE:
2106 case CHANNEL_ZONE2_BASS:
2107 case CHANNEL_ZONE2_TREBLE:
2108 case CHANNEL_ZONE2_BALANCE:
2109 case CHANNEL_ZONE2_FREQUENCY:
2112 case CHANNEL_ZONE3_POWER:
2113 case CHANNEL_ZONE3_SOURCE:
2114 case CHANNEL_ZONE3_VOLUME:
2115 case CHANNEL_ZONE3_MUTE:
2116 case CHANNEL_ZONE3_BASS:
2117 case CHANNEL_ZONE3_TREBLE:
2118 case CHANNEL_ZONE3_BALANCE:
2119 case CHANNEL_ZONE3_FREQUENCY:
2122 case CHANNEL_ZONE4_POWER:
2123 case CHANNEL_ZONE4_SOURCE:
2124 case CHANNEL_ZONE4_VOLUME:
2125 case CHANNEL_ZONE4_MUTE:
2126 case CHANNEL_ZONE4_BASS:
2127 case CHANNEL_ZONE4_TREBLE:
2128 case CHANNEL_ZONE4_BALANCE:
2129 case CHANNEL_ZONE4_FREQUENCY:
2137 case CHANNEL_MAIN_POWER:
2138 case CHANNEL_ALL_POWER:
2139 case CHANNEL_ZONE2_POWER:
2140 case CHANNEL_ZONE3_POWER:
2141 case CHANNEL_ZONE4_POWER:
2142 Boolean powerZone = powers[numZone];
2143 if (powerZone != null) {
2144 state = OnOffType.from(powerZone.booleanValue());
2147 case CHANNEL_SOURCE:
2148 case CHANNEL_MAIN_SOURCE:
2149 case CHANNEL_ZONE1_SOURCE:
2150 case CHANNEL_ZONE2_SOURCE:
2151 case CHANNEL_ZONE3_SOURCE:
2152 case CHANNEL_ZONE4_SOURCE:
2153 localSource = sources[numZone];
2154 if (isPowerOn(numZone) && localSource != null) {
2155 state = new StringType(localSource.getName());
2158 case CHANNEL_MAIN_RECORD_SOURCE:
2159 localSource = recordSource;
2160 if (isPowerOn() && localSource != null) {
2161 state = new StringType(localSource.getName());
2165 case CHANNEL_MAIN_DSP:
2167 state = new StringType(dsp.getName());
2170 case CHANNEL_VOLUME:
2171 case CHANNEL_MAIN_VOLUME:
2172 case CHANNEL_ZONE1_VOLUME:
2173 case CHANNEL_ZONE2_VOLUME:
2174 case CHANNEL_ZONE3_VOLUME:
2175 case CHANNEL_ZONE4_VOLUME:
2176 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2177 long volumePct = Math
2178 .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2179 state = new PercentType(BigDecimal.valueOf(volumePct));
2182 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2183 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2184 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2185 state = new DecimalType(volumes[numZone]);
2189 case CHANNEL_MAIN_MUTE:
2190 case CHANNEL_ZONE1_MUTE:
2191 case CHANNEL_ZONE2_MUTE:
2192 case CHANNEL_ZONE3_MUTE:
2193 case CHANNEL_ZONE4_MUTE:
2194 if (isPowerOn(numZone)) {
2195 state = OnOffType.from(mutes[numZone]);
2199 case CHANNEL_MAIN_BASS:
2200 case CHANNEL_ZONE1_BASS:
2201 case CHANNEL_ZONE2_BASS:
2202 case CHANNEL_ZONE3_BASS:
2203 case CHANNEL_ZONE4_BASS:
2204 if (isPowerOn(numZone)) {
2205 state = new DecimalType(basses[numZone]);
2208 case CHANNEL_TREBLE:
2209 case CHANNEL_MAIN_TREBLE:
2210 case CHANNEL_ZONE1_TREBLE:
2211 case CHANNEL_ZONE2_TREBLE:
2212 case CHANNEL_ZONE3_TREBLE:
2213 case CHANNEL_ZONE4_TREBLE:
2214 if (isPowerOn(numZone)) {
2215 state = new DecimalType(trebles[numZone]);
2219 if (isPowerOn() && track > 0) {
2220 state = new DecimalType(track);
2223 case CHANNEL_RANDOM:
2225 state = OnOffType.from(randomMode);
2228 case CHANNEL_REPEAT:
2230 state = new StringType(repeatMode.name());
2233 case CHANNEL_PLAY_CONTROL:
2235 switch (playStatus) {
2237 state = PlayPauseType.PLAY;
2241 state = PlayPauseType.PAUSE;
2246 case CHANNEL_FREQUENCY:
2247 case CHANNEL_ZONE1_FREQUENCY:
2248 case CHANNEL_ZONE2_FREQUENCY:
2249 case CHANNEL_ZONE3_FREQUENCY:
2250 case CHANNEL_ZONE4_FREQUENCY:
2251 if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
2252 state = new DecimalType(frequencies[numZone]);
2256 state = new StringType(frontPanelLine1);
2259 state = new StringType(frontPanelLine2);
2261 case CHANNEL_BRIGHTNESS:
2262 case CHANNEL_ALL_BRIGHTNESS:
2263 if (isPowerOn() && model.hasDimmerControl()) {
2264 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2265 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2266 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2269 case CHANNEL_TCBYPASS:
2271 state = OnOffType.from(tcbypass);
2274 case CHANNEL_BALANCE:
2275 case CHANNEL_ZONE1_BALANCE:
2276 case CHANNEL_ZONE2_BALANCE:
2277 case CHANNEL_ZONE3_BALANCE:
2278 case CHANNEL_ZONE4_BALANCE:
2279 if (isPowerOn(numZone)) {
2280 state = new DecimalType(balances[numZone]);
2283 case CHANNEL_SPEAKER_A:
2285 state = OnOffType.from(speakera);
2288 case CHANNEL_SPEAKER_B:
2290 state = OnOffType.from(speakerb);
2296 updateState(channel, state);
2300 * Inform about the device / main zone power state
2302 * @return true if device / main zone power state is known and known as ON
2304 private boolean isPowerOn() {
2305 return isPowerOn(0);
2309 * Inform about the power state
2311 * @param numZone the zone number (1-4) or 0 for the device or main zone
2313 * @return true if power state is known and known as ON
2315 private boolean isPowerOn(int numZone) {
2316 if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
2317 throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
2319 Boolean power = powers[numZone];
2320 return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
2324 * Get the command to be used for POWER ON
2326 * @param numZone the zone number (2-4) or 0 for the device or main zone
2328 * @return the command
2330 private RotelCommand getPowerOnCommand(int numZone) {
2333 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2335 return RotelCommand.ZONE2_POWER_ON;
2337 return RotelCommand.ZONE3_POWER_ON;
2339 return RotelCommand.ZONE4_POWER_ON;
2341 throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
2346 * Get the command to be used for POWER OFF
2348 * @param numZone the zone number (2-4) or 0 for the device or main zone
2350 * @return the command
2352 private RotelCommand getPowerOffCommand(int numZone) {
2355 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2357 return RotelCommand.ZONE2_POWER_OFF;
2359 return RotelCommand.ZONE3_POWER_OFF;
2361 return RotelCommand.ZONE4_POWER_OFF;
2363 throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
2368 * Get the command to be used for VOLUME UP
2370 * @param numZone the zone number (1-4) or 0 for the device or main zone
2372 * @return the command
2374 private RotelCommand getVolumeUpCommand(int numZone) {
2377 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2379 return RotelCommand.ZONE1_VOLUME_UP;
2381 return RotelCommand.ZONE2_VOLUME_UP;
2383 return RotelCommand.ZONE3_VOLUME_UP;
2385 return RotelCommand.ZONE4_VOLUME_UP;
2387 throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
2392 * Get the command to be used for VOLUME DOWN
2394 * @param numZone the zone number (1-4) or 0 for the device or main zone
2396 * @return the command
2398 private RotelCommand getVolumeDownCommand(int numZone) {
2401 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2402 : RotelCommand.VOLUME_DOWN;
2404 return RotelCommand.ZONE1_VOLUME_DOWN;
2406 return RotelCommand.ZONE2_VOLUME_DOWN;
2408 return RotelCommand.ZONE3_VOLUME_DOWN;
2410 return RotelCommand.ZONE4_VOLUME_DOWN;
2412 throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
2417 * Get the command to be used for VOLUME SET
2419 * @param numZone the zone number (1-4) or 0 for the device
2421 * @return the command
2423 private RotelCommand getVolumeSetCommand(int numZone) {
2426 return RotelCommand.VOLUME_SET;
2428 return RotelCommand.ZONE1_VOLUME_SET;
2430 return RotelCommand.ZONE2_VOLUME_SET;
2432 return RotelCommand.ZONE3_VOLUME_SET;
2434 return RotelCommand.ZONE4_VOLUME_SET;
2436 throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
2441 * Get the command to be used for MUTE ON
2443 * @param numZone the zone number (1-4) or 0 for the device or main zone
2445 * @return the command
2447 private RotelCommand getMuteOnCommand(int numZone) {
2450 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2452 return RotelCommand.ZONE1_MUTE_ON;
2454 return RotelCommand.ZONE2_MUTE_ON;
2456 return RotelCommand.ZONE3_MUTE_ON;
2458 return RotelCommand.ZONE4_MUTE_ON;
2460 throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
2465 * Get the command to be used for MUTE OFF
2467 * @param numZone the zone number (1-4) or 0 for the device or main zone
2469 * @return the command
2471 private RotelCommand getMuteOffCommand(int numZone) {
2474 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2476 return RotelCommand.ZONE1_MUTE_OFF;
2478 return RotelCommand.ZONE2_MUTE_OFF;
2480 return RotelCommand.ZONE3_MUTE_OFF;
2482 return RotelCommand.ZONE4_MUTE_OFF;
2484 throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
2489 * Get the command to be used for MUTE TOGGLE
2491 * @param numZone the zone number (1-4) or 0 for the device or main zone
2493 * @return the command
2495 private RotelCommand getMuteToggleCommand(int numZone) {
2498 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2499 : RotelCommand.MUTE_TOGGLE;
2501 return RotelCommand.ZONE1_MUTE_TOGGLE;
2503 return RotelCommand.ZONE2_MUTE_TOGGLE;
2505 return RotelCommand.ZONE3_MUTE_TOGGLE;
2507 return RotelCommand.ZONE4_MUTE_TOGGLE;
2509 throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
2514 * Get the command to be used for BASS UP
2516 * @param numZone the zone number (1-4) or 0 for the device
2518 * @return the command
2520 private RotelCommand getBassUpCommand(int numZone) {
2523 return RotelCommand.BASS_UP;
2525 return RotelCommand.ZONE1_BASS_UP;
2527 return RotelCommand.ZONE2_BASS_UP;
2529 return RotelCommand.ZONE3_BASS_UP;
2531 return RotelCommand.ZONE4_BASS_UP;
2533 throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
2538 * Get the command to be used for BASS DOWN
2540 * @param numZone the zone number (1-4) or 0 for the device
2542 * @return the command
2544 private RotelCommand getBassDownCommand(int numZone) {
2547 return RotelCommand.BASS_DOWN;
2549 return RotelCommand.ZONE1_BASS_DOWN;
2551 return RotelCommand.ZONE2_BASS_DOWN;
2553 return RotelCommand.ZONE3_BASS_DOWN;
2555 return RotelCommand.ZONE4_BASS_DOWN;
2557 throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
2562 * Get the command to be used for BASS SET
2564 * @param numZone the zone number (1-4) or 0 for the device
2566 * @return the command
2568 private RotelCommand getBassSetCommand(int numZone) {
2571 return RotelCommand.BASS_SET;
2573 return RotelCommand.ZONE1_BASS_SET;
2575 return RotelCommand.ZONE2_BASS_SET;
2577 return RotelCommand.ZONE3_BASS_SET;
2579 return RotelCommand.ZONE4_BASS_SET;
2581 throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
2586 * Get the command to be used for TREBLE UP
2588 * @param numZone the zone number (1-4) or 0 for the device
2590 * @return the command
2592 private RotelCommand getTrebleUpCommand(int numZone) {
2595 return RotelCommand.TREBLE_UP;
2597 return RotelCommand.ZONE1_TREBLE_UP;
2599 return RotelCommand.ZONE2_TREBLE_UP;
2601 return RotelCommand.ZONE3_TREBLE_UP;
2603 return RotelCommand.ZONE4_TREBLE_UP;
2605 throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
2610 * Get the command to be used for TREBLE DOWN
2612 * @param numZone the zone number (1-4) or 0 for the device
2614 * @return the command
2616 private RotelCommand getTrebleDownCommand(int numZone) {
2619 return RotelCommand.TREBLE_DOWN;
2621 return RotelCommand.ZONE1_TREBLE_DOWN;
2623 return RotelCommand.ZONE2_TREBLE_DOWN;
2625 return RotelCommand.ZONE3_TREBLE_DOWN;
2627 return RotelCommand.ZONE4_TREBLE_DOWN;
2629 throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
2634 * Get the command to be used for TREBLE SET
2636 * @param numZone the zone number (1-4) or 0 for the device
2638 * @return the command
2640 private RotelCommand getTrebleSetCommand(int numZone) {
2643 return RotelCommand.TREBLE_SET;
2645 return RotelCommand.ZONE1_TREBLE_SET;
2647 return RotelCommand.ZONE2_TREBLE_SET;
2649 return RotelCommand.ZONE3_TREBLE_SET;
2651 return RotelCommand.ZONE4_TREBLE_SET;
2653 throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
2658 * Get the command to be used for BALANCE LEFT
2660 * @param numZone the zone number (1-4) or 0 for the device
2662 * @return the command
2664 private RotelCommand getBalanceLeftCommand(int numZone) {
2667 return RotelCommand.BALANCE_LEFT;
2669 return RotelCommand.ZONE1_BALANCE_LEFT;
2671 return RotelCommand.ZONE2_BALANCE_LEFT;
2673 return RotelCommand.ZONE3_BALANCE_LEFT;
2675 return RotelCommand.ZONE4_BALANCE_LEFT;
2677 throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
2682 * Get the command to be used for BALANCE RIGHT
2684 * @param numZone the zone number (1-4) or 0 for the device
2686 * @return the command
2688 private RotelCommand getBalanceRightCommand(int numZone) {
2691 return RotelCommand.BALANCE_RIGHT;
2693 return RotelCommand.ZONE1_BALANCE_RIGHT;
2695 return RotelCommand.ZONE2_BALANCE_RIGHT;
2697 return RotelCommand.ZONE3_BALANCE_RIGHT;
2699 return RotelCommand.ZONE4_BALANCE_RIGHT;
2701 throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
2706 * Get the command to be used for BALANCE SET
2708 * @param numZone the zone number (1-4) or 0 for the device
2710 * @return the command
2712 private RotelCommand getBalanceSetCommand(int numZone) {
2715 return RotelCommand.BALANCE_SET;
2717 return RotelCommand.ZONE1_BALANCE_SET;
2719 return RotelCommand.ZONE2_BALANCE_SET;
2721 return RotelCommand.ZONE3_BALANCE_SET;
2723 return RotelCommand.ZONE4_BALANCE_SET;
2725 throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
2729 private void sendCommand(RotelCommand cmd) throws RotelException {
2730 sendCommand(cmd, null);
2734 * Request the Rotel device to execute a command
2736 * @param cmd the command to execute
2737 * @param value the integer value to consider for volume, bass or treble adjustment
2739 * @throws RotelException - In case of any problem
2741 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2744 message = protocolHandler.buildCommandMessage(cmd, value);
2745 } catch (RotelException e) {
2746 // Command not supported
2747 logger.debug("sendCommand: {}", e.getMessage());
2750 connector.writeOutput(cmd, message);
2752 if (connector instanceof RotelSimuConnector) {
2753 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2754 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2755 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2756 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);