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.RotelRepeatMode;
33 import org.openhab.binding.rotel.internal.RotelStateDescriptionOptionProvider;
34 import org.openhab.binding.rotel.internal.communication.RotelCommand;
35 import org.openhab.binding.rotel.internal.communication.RotelConnector;
36 import org.openhab.binding.rotel.internal.communication.RotelDsp;
37 import org.openhab.binding.rotel.internal.communication.RotelIpConnector;
38 import org.openhab.binding.rotel.internal.communication.RotelSerialConnector;
39 import org.openhab.binding.rotel.internal.communication.RotelSimuConnector;
40 import org.openhab.binding.rotel.internal.communication.RotelSource;
41 import org.openhab.binding.rotel.internal.configuration.RotelThingConfiguration;
42 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
43 import org.openhab.binding.rotel.internal.protocol.RotelMessageEvent;
44 import org.openhab.binding.rotel.internal.protocol.RotelMessageEventListener;
45 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
46 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV1ProtocolHandler;
47 import org.openhab.binding.rotel.internal.protocol.ascii.RotelAsciiV2ProtocolHandler;
48 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
49 import org.openhab.core.io.transport.serial.SerialPortManager;
50 import org.openhab.core.library.types.DecimalType;
51 import org.openhab.core.library.types.IncreaseDecreaseType;
52 import org.openhab.core.library.types.NextPreviousType;
53 import org.openhab.core.library.types.OnOffType;
54 import org.openhab.core.library.types.PercentType;
55 import org.openhab.core.library.types.PlayPauseType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.Thing;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.binding.BaseThingHandler;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.StateOption;
66 import org.openhab.core.types.UnDefType;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
71 * The {@link RotelHandler} is responsible for handling commands, which are sent to one of the channels.
73 * @author Laurent Garnier - Initial contribution
76 public class RotelHandler extends BaseThingHandler implements RotelMessageEventListener {
78 private final Logger logger = LoggerFactory.getLogger(RotelHandler.class);
80 private static final RotelModel DEFAULT_MODEL = RotelModel.RSP1066;
81 private static final long POLLING_INTERVAL = TimeUnit.SECONDS.toSeconds(60);
82 private static final boolean USE_SIMULATED_DEVICE = false;
83 private static final int SLEEP_INTV = 30;
85 private @Nullable ScheduledFuture<?> reconnectJob;
86 private @Nullable ScheduledFuture<?> powerOffJob;
87 private @Nullable ScheduledFuture<?>[] powerOnZoneJobs = { null, null, null, null, null };
89 private RotelStateDescriptionOptionProvider stateDescriptionProvider;
90 private SerialPortManager serialPortManager;
92 private RotelModel model;
93 private RotelProtocol protocol;
94 private RotelAbstractProtocolHandler protocolHandler;
95 private RotelConnector connector;
97 private int minVolume;
98 private int maxVolume;
99 private int minToneLevel;
100 private int maxToneLevel;
102 private int currentZone = 1;
103 private boolean selectingRecord;
104 private @Nullable Boolean[] powers = { null, false, false, false, false };
105 private boolean powerControlPerZone;
106 private @Nullable RotelSource recordSource;
107 private @Nullable RotelSource[] sources = { RotelSource.CAT0_CD, null, null, null, null };
108 private RotelDsp dsp = RotelDsp.CAT1_NONE;
109 private boolean[] fixedVolumeZones = { false, false, false, false, false };
110 private int[] volumes = { 0, 0, 0, 0, 0 };
111 private boolean[] mutes = { false, false, false, false, false };
112 private int[] basses = { 0, 0, 0, 0, 0 };
113 private int[] trebles = { 0, 0, 0, 0, 0 };
114 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
116 private boolean randomMode;
117 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
118 private double[] frequencies = { 0.0, 0.0, 0.0, 0.0, 0.0 };
119 private String frontPanelLine1 = "";
120 private String frontPanelLine2 = "";
121 private int brightness;
122 private boolean tcbypass;
123 private int[] balances = { 0, 0, 0, 0, 0 };
124 private int minBalanceLevel;
125 private int maxBalanceLevel;
126 private boolean speakera;
127 private boolean speakerb;
129 private Object sequenceLock = new Object();
134 public RotelHandler(Thing thing, RotelStateDescriptionOptionProvider stateDescriptionProvider,
135 SerialPortManager serialPortManager) {
137 this.stateDescriptionProvider = stateDescriptionProvider;
138 this.serialPortManager = serialPortManager;
139 this.model = DEFAULT_MODEL;
140 this.protocolHandler = new RotelHexProtocolHandler(model, Map.of());
141 this.protocol = protocolHandler.getProtocol();
142 this.connector = new RotelSimuConnector(model, protocolHandler, new HashMap<>(), "OH-binding-rotel");
146 public void initialize() {
147 logger.debug("Start initializing handler for thing {}", getThing().getUID());
149 switch (getThing().getThingTypeUID().getId()) {
150 case THING_TYPE_ID_RSP1066:
151 model = RotelModel.RSP1066;
153 case THING_TYPE_ID_RSP1068:
154 model = RotelModel.RSP1068;
156 case THING_TYPE_ID_RSP1069:
157 model = RotelModel.RSP1069;
159 case THING_TYPE_ID_RSP1098:
160 model = RotelModel.RSP1098;
162 case THING_TYPE_ID_RSP1570:
163 model = RotelModel.RSP1570;
165 case THING_TYPE_ID_RSP1572:
166 model = RotelModel.RSP1572;
168 case THING_TYPE_ID_RSX1055:
169 model = RotelModel.RSX1055;
171 case THING_TYPE_ID_RSX1056:
172 model = RotelModel.RSX1056;
174 case THING_TYPE_ID_RSX1057:
175 model = RotelModel.RSX1057;
177 case THING_TYPE_ID_RSX1058:
178 model = RotelModel.RSX1058;
180 case THING_TYPE_ID_RSX1065:
181 model = RotelModel.RSX1065;
183 case THING_TYPE_ID_RSX1067:
184 model = RotelModel.RSX1067;
186 case THING_TYPE_ID_RSX1550:
187 model = RotelModel.RSX1550;
189 case THING_TYPE_ID_RSX1560:
190 model = RotelModel.RSX1560;
192 case THING_TYPE_ID_RSX1562:
193 model = RotelModel.RSX1562;
195 case THING_TYPE_ID_A11:
196 model = RotelModel.A11;
198 case THING_TYPE_ID_A12:
199 model = RotelModel.A12;
201 case THING_TYPE_ID_A14:
202 model = RotelModel.A14;
204 case THING_TYPE_ID_CD11:
205 model = RotelModel.CD11;
207 case THING_TYPE_ID_CD14:
208 model = RotelModel.CD14;
210 case THING_TYPE_ID_RA11:
211 model = RotelModel.RA11;
213 case THING_TYPE_ID_RA12:
214 model = RotelModel.RA12;
216 case THING_TYPE_ID_RA1570:
217 model = RotelModel.RA1570;
219 case THING_TYPE_ID_RA1572:
220 model = RotelModel.RA1572;
222 case THING_TYPE_ID_RA1592:
223 model = RotelModel.RA1592;
225 case THING_TYPE_ID_RAP1580:
226 model = RotelModel.RAP1580;
228 case THING_TYPE_ID_RC1570:
229 model = RotelModel.RC1570;
231 case THING_TYPE_ID_RC1572:
232 model = RotelModel.RC1572;
234 case THING_TYPE_ID_RC1590:
235 model = RotelModel.RC1590;
237 case THING_TYPE_ID_RCD1570:
238 model = RotelModel.RCD1570;
240 case THING_TYPE_ID_RCD1572:
241 model = RotelModel.RCD1572;
243 case THING_TYPE_ID_RCX1500:
244 model = RotelModel.RCX1500;
246 case THING_TYPE_ID_RDD1580:
247 model = RotelModel.RDD1580;
249 case THING_TYPE_ID_RDG1520:
250 case THING_TYPE_ID_RT09:
251 model = RotelModel.RDG1520;
253 case THING_TYPE_ID_RSP1576:
254 model = RotelModel.RSP1576;
256 case THING_TYPE_ID_RSP1582:
257 model = RotelModel.RSP1582;
259 case THING_TYPE_ID_RT11:
260 model = RotelModel.RT11;
262 case THING_TYPE_ID_RT1570:
263 model = RotelModel.RT1570;
265 case THING_TYPE_ID_T11:
266 model = RotelModel.T11;
268 case THING_TYPE_ID_T14:
269 model = RotelModel.T14;
271 case THING_TYPE_ID_C8:
272 model = RotelModel.C8;
274 case THING_TYPE_ID_M8:
275 model = RotelModel.M8;
277 case THING_TYPE_ID_P5:
278 model = RotelModel.P5;
280 case THING_TYPE_ID_S5:
281 model = RotelModel.S5;
283 case THING_TYPE_ID_X3:
284 model = RotelModel.X3;
286 case THING_TYPE_ID_X5:
287 model = RotelModel.X5;
290 model = DEFAULT_MODEL;
294 RotelThingConfiguration config = getConfigAs(RotelThingConfiguration.class);
296 protocol = RotelProtocol.HEX;
297 if (config.protocol != null && !config.protocol.isEmpty()) {
299 protocol = RotelProtocol.getFromName(config.protocol);
300 } catch (RotelException e) {
301 // Invalid protocol name in configuration, HEX will be considered by default
304 Map<String, String> properties = editProperties();
305 String property = properties.get(RotelBindingConstants.PROPERTY_PROTOCOL);
306 if (property != null && !property.isEmpty()) {
308 protocol = RotelProtocol.getFromName(property);
309 } catch (RotelException e) {
310 // Invalid protocol name in thing property, HEX will be considered by default
314 logger.debug("rotelProtocol {}", protocol.getName());
316 Map<RotelSource, String> sourcesCustomLabels = new HashMap<>();
317 Map<RotelSource, String> sourcesLabels = new HashMap<>();
319 String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
321 if (model.hasVolumeControl()) {
322 maxVolume = model.getVolumeMax();
323 if (!model.hasDirectVolumeControl()) {
325 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your volume item.",
326 minVolume, maxVolume);
329 if (model.hasToneControl()) {
330 maxToneLevel = model.getToneLevelMax();
331 minToneLevel = -maxToneLevel;
333 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
334 minToneLevel, maxToneLevel);
336 if (model.hasBalanceControl()) {
337 maxBalanceLevel = model.getBalanceLevelMax();
338 minBalanceLevel = -maxBalanceLevel;
339 logger.info("Set minValue to {} and maxValue to {} for your sitemap widget attached to your balance item.",
340 minBalanceLevel, maxBalanceLevel);
343 powerControlPerZone = model.hasPowerControlPerZone();
345 // Check configuration settings
346 String configError = null;
347 if ((config.serialPort == null || config.serialPort.isEmpty())
348 && (config.host == null || config.host.isEmpty())) {
349 configError = "@text/offline.config-error-unknown-serialport-and-host";
350 } else if (config.host == null || config.host.isEmpty()) {
351 if (config.serialPort.toLowerCase().startsWith("rfc2217")) {
352 configError = "@text/offline.config-error-invalid-serial-over-ip";
355 if (config.port == null) {
356 configError = "@text/offline.config-error-unknown-port";
357 } else if (config.port <= 0) {
358 configError = "@text/offline.config-error-invalid-port";
362 if (configError != null) {
363 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
365 for (RotelSource src : model.getSources()) {
366 // Consider custom input labels
368 switch (src.getName()) {
370 label = config.inputLabelCd;
373 label = config.inputLabelTuner;
376 label = config.inputLabelTape;
379 label = config.inputLabelPhono;
382 label = config.inputLabelVideo1;
385 label = config.inputLabelVideo2;
388 label = config.inputLabelVideo3;
391 label = config.inputLabelVideo4;
394 label = config.inputLabelVideo5;
397 label = config.inputLabelVideo6;
400 label = config.inputLabelUsb;
403 label = config.inputLabelMulti;
408 if (label != null && !label.isEmpty()) {
409 sourcesCustomLabels.put(src, label);
411 sourcesLabels.put(src, (label == null || label.isEmpty()) ? src.getLabel() : label);
414 if (protocol == RotelProtocol.HEX) {
415 protocolHandler = new RotelHexProtocolHandler(model, sourcesLabels);
416 } else if (protocol == RotelProtocol.ASCII_V1) {
417 protocolHandler = new RotelAsciiV1ProtocolHandler(model);
419 protocolHandler = new RotelAsciiV2ProtocolHandler(model);
422 if (USE_SIMULATED_DEVICE) {
423 connector = new RotelSimuConnector(model, protocolHandler, sourcesLabels, readerThreadName);
424 } else if (config.serialPort != null) {
425 connector = new RotelSerialConnector(serialPortManager, config.serialPort, model.getBaudRate(),
426 protocolHandler, readerThreadName);
428 connector = new RotelIpConnector(config.host, config.port, protocolHandler, readerThreadName);
431 if (model.hasSourceControl()) {
432 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
433 getStateOptions(model.getSources(), sourcesCustomLabels));
434 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_SOURCE),
435 getStateOptions(model.getSources(), sourcesCustomLabels));
436 stateDescriptionProvider.setStateOptions(
437 new ChannelUID(getThing().getUID(), CHANNEL_MAIN_RECORD_SOURCE),
438 getStateOptions(model.getRecordSources(), sourcesCustomLabels));
440 if (model.hasZoneSourceControl(1)) {
441 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE1_SOURCE),
442 getStateOptions(model.getZoneSources(1), sourcesCustomLabels));
444 if (model.hasZoneSourceControl(2)) {
445 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE2_SOURCE),
446 getStateOptions(model.getZoneSources(2), sourcesCustomLabels));
448 if (model.hasZoneSourceControl(3)) {
449 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE3_SOURCE),
450 getStateOptions(model.getZoneSources(3), sourcesCustomLabels));
452 if (model.hasZoneSourceControl(4)) {
453 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_ZONE4_SOURCE),
454 getStateOptions(model.getZoneSources(4), sourcesCustomLabels));
456 if (model.hasDspControl()) {
457 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_DSP),
458 model.getDspStateOptions());
459 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_MAIN_DSP),
460 model.getDspStateOptions());
463 updateStatus(ThingStatus.UNKNOWN);
465 scheduleReconnectJob();
468 logger.debug("Finished initializing!");
472 public void dispose() {
473 logger.debug("Disposing handler for thing {}", getThing().getUID());
475 for (int zone = 0; zone <= model.getNumberOfZones(); zone++) {
476 cancelPowerOnZoneJob(zone);
478 cancelReconnectJob();
483 public List<StateOption> getStateOptions(List<RotelSource> list, Map<RotelSource, String> sourcesLabels) {
484 List<StateOption> options = new ArrayList<>();
485 for (RotelSource item : list) {
486 String label = sourcesLabels.get(item);
487 options.add(new StateOption(item.getName(), label == null ? ("@text/source." + item.getName()) : label));
493 public void handleCommand(ChannelUID channelUID, Command command) {
494 String channel = channelUID.getId();
496 if (getThing().getStatus() != ThingStatus.ONLINE) {
497 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
501 if (command instanceof RefreshType) {
502 updateChannelState(channel);
506 if (!connector.isConnected()) {
507 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
513 case CHANNEL_ZONE1_SOURCE:
514 case CHANNEL_ZONE1_VOLUME:
515 case CHANNEL_ZONE1_MUTE:
516 case CHANNEL_ZONE1_BASS:
517 case CHANNEL_ZONE1_TREBLE:
518 case CHANNEL_ZONE1_BALANCE:
521 case CHANNEL_ZONE2_POWER:
522 case CHANNEL_ZONE2_SOURCE:
523 case CHANNEL_ZONE2_VOLUME:
524 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
525 case CHANNEL_ZONE2_MUTE:
526 case CHANNEL_ZONE2_BASS:
527 case CHANNEL_ZONE2_TREBLE:
528 case CHANNEL_ZONE2_BALANCE:
531 case CHANNEL_ZONE3_POWER:
532 case CHANNEL_ZONE3_SOURCE:
533 case CHANNEL_ZONE3_VOLUME:
534 case CHANNEL_ZONE3_MUTE:
535 case CHANNEL_ZONE3_BASS:
536 case CHANNEL_ZONE3_TREBLE:
537 case CHANNEL_ZONE3_BALANCE:
540 case CHANNEL_ZONE4_POWER:
541 case CHANNEL_ZONE4_SOURCE:
542 case CHANNEL_ZONE4_VOLUME:
543 case CHANNEL_ZONE4_MUTE:
544 case CHANNEL_ZONE4_BASS:
545 case CHANNEL_ZONE4_TREBLE:
546 case CHANNEL_ZONE4_BALANCE:
555 boolean success = true;
556 synchronized (sequenceLock) {
560 case CHANNEL_MAIN_POWER:
561 case CHANNEL_ZONE2_POWER:
562 case CHANNEL_ZONE3_POWER:
563 case CHANNEL_ZONE4_POWER:
564 if (numZone == 0 || model.hasZoneCommands(numZone)) {
565 handlePowerCmd(channel, command, getPowerOnCommand(numZone), getPowerOffCommand(numZone));
566 } else if (numZone == 2 && model.getNumberOfZones() == 2) {
567 if (isPowerOn() || isPowerOn(numZone)) {
568 selectZone(2, model.getZoneSelectCmd());
570 sendCommand(RotelCommand.ZONE_SELECT);
573 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
576 case CHANNEL_ALL_POWER:
577 handlePowerCmd(channel, command, RotelCommand.POWER_ON, RotelCommand.POWER_OFF);
580 case CHANNEL_MAIN_SOURCE:
581 case CHANNEL_ZONE1_SOURCE:
582 case CHANNEL_ZONE2_SOURCE:
583 case CHANNEL_ZONE3_SOURCE:
584 case CHANNEL_ZONE4_SOURCE:
585 if (!isPowerOn(numZone)) {
587 logger.debug("Command {} from channel {} ignored: {} in standby", command, channel,
588 numZone == 0 ? "device" : "zone " + numZone);
589 } else if (numZone == 0 || model.hasZoneCommands(numZone)) {
590 src = model.getSourceFromName(command.toString());
592 cmd = model.hasOtherThanPrimaryCommands() ? src.getZoneCommand(1) : src.getCommand();
594 cmd = src.getZoneCommand(numZone);
598 if (model.canGetFrequency()) {
599 // send <new-source> returns
600 // 1.) the selected <new-source>
601 // 2.) the used frequency
603 // at response-time the frequency has the value of <old-source>
604 // so we must wait a short moment to get the frequency of <new-source>
606 sendCommand(RotelCommand.FREQUENCY);
608 updateChannelState(CHANNEL_FREQUENCY);
612 logger.debug("Command {} from channel {} failed: undefined source command", command,
615 } else if (numZone == 2 && model.getNumberOfZones() > 1) {
616 src = model.getSourceFromName(command.toString());
617 cmd = src.getCommand();
619 selectZone(2, model.getZoneSelectCmd());
621 if (model.canGetFrequency()) {
622 // send <new-source> returns
623 // 1.) the selected <new-source>
624 // 2.) the used frequency
626 // at response-time the frequency has the value of <old-source>
627 // so we must wait a short moment to get the frequency of <new-source>
629 sendCommand(RotelCommand.FREQUENCY);
631 updateChannelState(CHANNEL_FREQUENCY);
635 logger.debug("Command {} from channel {} failed: undefined source command", command,
640 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
643 case CHANNEL_MAIN_RECORD_SOURCE:
646 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
647 } else if (model.hasOtherThanPrimaryCommands()) {
648 src = model.getSourceFromName(command.toString());
649 cmd = src.getRecordCommand();
654 logger.debug("Command {} from channel {} failed: undefined record source command",
658 src = model.getSourceFromName(command.toString());
659 cmd = src.getCommand();
661 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
666 logger.debug("Command {} from channel {} failed: undefined source command", command,
672 case CHANNEL_MAIN_DSP:
675 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
677 sendCommand(model.getCommandFromDspName(command.toString()));
681 case CHANNEL_MAIN_VOLUME:
682 case CHANNEL_MAIN_VOLUME_UP_DOWN:
683 case CHANNEL_ZONE1_VOLUME:
684 case CHANNEL_ZONE2_VOLUME:
685 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
686 case CHANNEL_ZONE3_VOLUME:
687 case CHANNEL_ZONE4_VOLUME:
688 if (!isPowerOn(numZone)) {
690 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
691 numZone == 0 ? "device" : "zone " + numZone);
692 } else if (fixedVolumeZones[numZone]) {
694 logger.debug("Command {} from channel {} ignored: fixed volume", command, channel);
695 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
696 handleVolumeCmd(volumes[numZone], channel, command, getVolumeUpCommand(numZone),
697 getVolumeDownCommand(numZone),
698 CHANNEL_MAIN_VOLUME_UP_DOWN.equals(channel)
699 || CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null
700 : getVolumeSetCommand(numZone));
701 } else if (numZone == 2 && model.hasVolumeControl() && model.getNumberOfZones() > 1) {
702 selectZone(2, model.getZoneSelectCmd());
703 handleVolumeCmd(volumes[numZone], channel, command, RotelCommand.VOLUME_UP,
704 RotelCommand.VOLUME_DOWN,
705 CHANNEL_ZONE2_VOLUME_UP_DOWN.equals(channel) ? null : RotelCommand.VOLUME_SET);
708 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
712 case CHANNEL_MAIN_MUTE:
713 case CHANNEL_ZONE1_MUTE:
714 case CHANNEL_ZONE2_MUTE:
715 case CHANNEL_ZONE3_MUTE:
716 case CHANNEL_ZONE4_MUTE:
717 if (!isPowerOn(numZone)) {
719 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
720 numZone == 0 ? "device" : "zone " + numZone);
721 } else if (model.hasVolumeControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
722 handleMuteCmd(numZone == 0 && protocol == RotelProtocol.HEX, channel, command,
723 getMuteOnCommand(numZone), getMuteOffCommand(numZone),
724 getMuteToggleCommand(numZone));
727 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
731 case CHANNEL_MAIN_BASS:
732 case CHANNEL_ZONE1_BASS:
733 case CHANNEL_ZONE2_BASS:
734 case CHANNEL_ZONE3_BASS:
735 case CHANNEL_ZONE4_BASS:
736 if (!isPowerOn(numZone)) {
738 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
739 numZone == 0 ? "device" : "zone " + numZone);
740 } else if (tcbypass) {
742 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
744 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
745 handleToneCmd(basses[numZone], channel, command, 2, getBassUpCommand(numZone),
746 getBassDownCommand(numZone), getBassSetCommand(numZone));
749 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
753 case CHANNEL_MAIN_TREBLE:
754 case CHANNEL_ZONE1_TREBLE:
755 case CHANNEL_ZONE2_TREBLE:
756 case CHANNEL_ZONE3_TREBLE:
757 case CHANNEL_ZONE4_TREBLE:
758 if (!isPowerOn(numZone)) {
760 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
761 numZone == 0 ? "device" : "zone " + numZone);
762 } else if (tcbypass) {
764 logger.debug("Command {} from channel {} ignored: tone control bypass is ON", command,
766 } else if (model.hasToneControl() && (numZone == 0 || model.hasZoneCommands(numZone))) {
767 handleToneCmd(trebles[numZone], channel, command, 1, getTrebleUpCommand(numZone),
768 getTrebleDownCommand(numZone), getTrebleSetCommand(numZone));
771 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
774 case CHANNEL_PLAY_CONTROL:
777 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
778 } else if (command instanceof PlayPauseType && command == PlayPauseType.PLAY) {
779 sendCommand(RotelCommand.PLAY);
780 } else if (command instanceof PlayPauseType && command == PlayPauseType.PAUSE) {
781 sendCommand(RotelCommand.PAUSE);
782 if (protocol == RotelProtocol.ASCII_V1 && model != RotelModel.RCD1570
783 && model != RotelModel.RCD1572 && model != RotelModel.RCX1500) {
784 Thread.sleep(SLEEP_INTV);
785 sendCommand(RotelCommand.PLAY_STATUS);
787 } else if (command instanceof NextPreviousType && command == NextPreviousType.NEXT) {
788 sendCommand(RotelCommand.TRACK_FORWARD);
789 } else if (command instanceof NextPreviousType && command == NextPreviousType.PREVIOUS) {
790 sendCommand(RotelCommand.TRACK_BACKWORD);
793 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
799 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
800 } else if (command instanceof OnOffType) {
801 sendCommand(RotelCommand.RANDOM_TOGGLE);
804 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
810 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
812 RotelRepeatMode currentMode = repeatMode;
813 RotelRepeatMode mode = RotelRepeatMode.OFF;
815 mode = RotelRepeatMode.getFromName(command.toString());
816 if (mode == currentMode) {
818 logger.debug("Command {} from channel {} ignored: no change requested", command,
821 } catch (RotelException e) {
823 logger.debug("Command {} from channel {} failed: invalid command value", command,
827 // Toggle TRACK -> DISC -> OFF
828 sendCommand(RotelCommand.REPEAT_TOGGLE);
829 if ((mode == RotelRepeatMode.OFF && currentMode == RotelRepeatMode.TRACK)
830 || (mode == RotelRepeatMode.TRACK && currentMode == RotelRepeatMode.DISC)
831 || (mode == RotelRepeatMode.DISC && currentMode == RotelRepeatMode.OFF)) {
832 Thread.sleep(SLEEP_INTV);
833 sendCommand(RotelCommand.REPEAT_TOGGLE);
838 case CHANNEL_BRIGHTNESS:
839 case CHANNEL_ALL_BRIGHTNESS:
842 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
843 } else if (!model.hasDimmerControl()) {
845 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
846 } else if (command instanceof PercentType) {
847 int dimmer = (int) Math.round(((PercentType) command).doubleValue() / 100.0
848 * (model.getDimmerLevelMax() - model.getDimmerLevelMin()))
849 + model.getDimmerLevelMin();
850 sendCommand(RotelCommand.DIMMER_LEVEL_SET, dimmer);
853 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
856 case CHANNEL_TCBYPASS:
859 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
860 } else if (!model.hasToneControl() || protocol == RotelProtocol.HEX) {
862 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
864 handleTcbypassCmd(channel, command,
865 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_OFF
866 : RotelCommand.TCBYPASS_ON,
867 protocol == RotelProtocol.ASCII_V1 ? RotelCommand.TONE_CONTROLS_ON
868 : RotelCommand.TCBYPASS_OFF);
871 case CHANNEL_BALANCE:
872 case CHANNEL_ZONE1_BALANCE:
873 case CHANNEL_ZONE2_BALANCE:
874 case CHANNEL_ZONE3_BALANCE:
875 case CHANNEL_ZONE4_BALANCE:
876 if (!isPowerOn(numZone)) {
878 logger.debug("Command {} from channel {} ignored: zone {} in standby", command, channel,
879 numZone == 0 ? "device" : "zone " + numZone);
880 } else if (!model.hasBalanceControl() || protocol == RotelProtocol.HEX) {
882 logger.debug("Command {} from channel {} failed: unavailable feature", command, channel);
884 handleBalanceCmd(channel, command, getBalanceLeftCommand(numZone),
885 getBalanceRightCommand(numZone), getBalanceSetCommand(numZone));
888 case CHANNEL_SPEAKER_A:
891 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
893 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_A_ON,
894 RotelCommand.SPEAKER_A_OFF, RotelCommand.SPEAKER_A_TOGGLE);
897 case CHANNEL_SPEAKER_B:
900 logger.debug("Command {} from channel {} ignored: device in standby", command, channel);
902 handleSpeakerCmd(protocol == RotelProtocol.HEX, channel, command, RotelCommand.SPEAKER_B_ON,
903 RotelCommand.SPEAKER_B_OFF, RotelCommand.SPEAKER_B_TOGGLE);
908 logger.debug("Command {} from channel {} failed: nnexpected command", command, channel);
912 logger.debug("Command {} from channel {} succeeded", command, channel);
914 updateChannelState(channel);
916 } catch (RotelException e) {
917 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
918 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
919 "@text/offline.comm-error-sending-command");
921 scheduleReconnectJob();
922 } catch (InterruptedException e) {
923 logger.debug("Command {} from channel {} interrupted: {}", command, channel, e.getMessage());
924 Thread.currentThread().interrupt();
930 * Handle a power ON/OFF command
932 * @param channel the channel
933 * @param command the received channel command (OnOffType)
934 * @param onCmd the command to be sent to the device to power it ON
935 * @param offCmd the command to be sent to the device to power it OFF
937 * @throws RotelException in case of communication error with the device
939 private void handlePowerCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
940 throws RotelException {
941 if (command instanceof OnOffType && command == OnOffType.ON) {
943 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
946 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
951 * Handle a volume command
953 * @param current the current volume
954 * @param channel the channel
955 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
956 * @param upCmd the command to be sent to the device to increase the volume
957 * @param downCmd the command to be sent to the device to decrease the volume
958 * @param setCmd the command to be sent to the device to set the volume at a value
960 * @throws RotelException in case of communication error with the device
962 private void handleVolumeCmd(int current, String channel, Command command, RotelCommand upCmd, RotelCommand downCmd,
963 @Nullable RotelCommand setCmd) throws RotelException {
964 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
966 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
967 sendCommand(downCmd);
968 } else if (command instanceof DecimalType && setCmd == null) {
969 int value = ((DecimalType) command).intValue();
970 if (value >= minVolume && value <= maxVolume) {
971 if (value > current) {
973 } else if (value < current) {
974 sendCommand(downCmd);
977 } else if (command instanceof PercentType && setCmd != null) {
978 int value = (int) Math.round(((PercentType) command).doubleValue() / 100.0 * (maxVolume - minVolume))
980 sendCommand(setCmd, value);
982 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
987 * Handle a mute command
989 * @param onlyToggle true if only the toggle command must be used
990 * @param channel the channel
991 * @param command the received channel command (OnOffType)
992 * @param onCmd the command to be sent to the device to mute
993 * @param offCmd the command to be sent to the device to unmute
994 * @param toggleCmd the command to be sent to the device to toggle the mute state
996 * @throws RotelException in case of communication error with the device
998 private void handleMuteCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
999 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1000 if (command instanceof OnOffType) {
1002 sendCommand(toggleCmd);
1003 } else if (command == OnOffType.ON) {
1005 } else if (command == OnOffType.OFF) {
1006 sendCommand(offCmd);
1009 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1014 * Handle a tone level adjustment command (bass or treble)
1016 * @param current the current tone level
1017 * @param channel the channel
1018 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1019 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1020 * @param upCmd the command to be sent to the device to increase the tone level
1021 * @param downCmd the command to be sent to the device to decrease the tone level
1022 * @param setCmd the command to be sent to the device to set the tone level at a value
1024 * @throws RotelException in case of communication error with the device
1025 * @throws InterruptedException in case of interruption during a thread sleep
1027 private void handleToneCmd(int current, String channel, Command command, int nbSelect, RotelCommand upCmd,
1028 RotelCommand downCmd, RotelCommand setCmd) throws RotelException, InterruptedException {
1029 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1030 selectToneControl(nbSelect);
1032 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1033 selectToneControl(nbSelect);
1034 sendCommand(downCmd);
1035 } else if (command instanceof DecimalType) {
1036 int value = ((DecimalType) command).intValue();
1037 if (value >= minToneLevel && value <= maxToneLevel) {
1038 if (protocol != RotelProtocol.HEX) {
1039 sendCommand(setCmd, value);
1040 } else if (value > current) {
1041 selectToneControl(nbSelect);
1043 } else if (value < current) {
1044 selectToneControl(nbSelect);
1045 sendCommand(downCmd);
1049 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1054 * Handle a tcbypass command (only for ASCII protocol)
1056 * @param channel the channel
1057 * @param command the received channel command (OnOffType)
1058 * @param onCmd the command to be sent to the device to bypass_on
1059 * @param offCmd the command to be sent to the device to bypass_off
1061 * @throws RotelException in case of communication error with the device
1063 private void handleTcbypassCmd(String channel, Command command, RotelCommand onCmd, RotelCommand offCmd)
1064 throws RotelException, InterruptedException {
1065 if (command instanceof OnOffType) {
1066 if (command == OnOffType.ON) {
1070 updateChannelState(CHANNEL_BASS);
1071 updateChannelState(CHANNEL_TREBLE);
1072 } else if (command == OnOffType.OFF) {
1073 sendCommand(offCmd);
1075 sendCommand(RotelCommand.BASS);
1077 sendCommand(RotelCommand.TREBLE);
1080 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1085 * Handle a speaker command
1087 * @param onlyToggle true if only the toggle command must be used
1088 * @param channel the channel
1089 * @param command the received channel command (OnOffType)
1090 * @param onCmd the command to be sent to the device to speaker_x_on
1091 * @param offCmd the command to be sent to the device to speaker_x_off
1092 * @param toggleCmd the command to be sent to the device to toggle the speaker_x state
1094 * @throws RotelException in case of communication error with the device
1096 private void handleSpeakerCmd(boolean onlyToggle, String channel, Command command, RotelCommand onCmd,
1097 RotelCommand offCmd, RotelCommand toggleCmd) throws RotelException {
1098 if (command instanceof OnOffType) {
1100 sendCommand(toggleCmd);
1101 } else if (command == OnOffType.ON) {
1103 } else if (command == OnOffType.OFF) {
1104 sendCommand(offCmd);
1107 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1112 * Handle a tone balance adjustment command (left or right) (only for ASCII protocol)
1114 * @param channel the channel
1115 * @param command the received channel command (IncreaseDecreaseType or DecimalType)
1116 * @param rightCmd the command to be sent to the device to "increase" balance (shift to the right side)
1117 * @param leftCmd the command to be sent to the device to "decrease" balance (shift to the left side)
1118 * @param setCmd the command to be sent to the device to set the balance at a value
1120 * @throws RotelException in case of communication error with the device
1121 * @throws InterruptedException in case of interruption during a thread sleep
1123 private void handleBalanceCmd(String channel, Command command, RotelCommand leftCmd, RotelCommand rightCmd,
1124 RotelCommand setCmd) throws RotelException, InterruptedException {
1125 if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
1126 sendCommand(rightCmd);
1127 } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
1128 sendCommand(leftCmd);
1129 } else if (command instanceof DecimalType) {
1130 int value = ((DecimalType) command).intValue();
1131 if (value >= minBalanceLevel && value <= maxBalanceLevel) {
1132 sendCommand(setCmd, value);
1135 logger.debug("Command {} from channel {} failed: invalid command value", command, channel);
1140 * Run a sequence of commands to display the current tone level (bass or treble) on the device front panel
1142 * @param nbSelect the number of TONE_CONTROL_SELECT commands to be run to display the right tone (bass or treble)
1144 * @throws RotelException in case of communication error with the device
1145 * @throws InterruptedException in case of interruption during a thread sleep
1147 private void selectToneControl(int nbSelect) throws RotelException, InterruptedException {
1148 // No tone control select command for RSX-1065
1149 if (protocol == RotelProtocol.HEX && model != RotelModel.RSX1065) {
1150 selectFeature(nbSelect, RotelCommand.RECORD_FONCTION_SELECT, RotelCommand.TONE_CONTROL_SELECT);
1155 * Run a sequence of commands to display a particular zone on the device front panel
1157 * @param zone the zone to be displayed (1 for main zone)
1158 * @param selectCommand the command to be sent to the device to switch the display between zones
1160 * @throws RotelException in case of communication error with the device
1161 * @throws InterruptedException in case of interruption during a thread sleep
1163 private void selectZone(int zone, @Nullable RotelCommand selectCommand)
1164 throws RotelException, InterruptedException {
1165 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() > 1 && zone >= 1 && zone != currentZone
1166 && selectCommand != null) {
1168 if (zone < currentZone) {
1169 nbSelect = zone + model.getNumberOfZones() - 1 - currentZone;
1170 if (isPowerOn() && selectCommand == RotelCommand.RECORD_FONCTION_SELECT) {
1174 nbSelect = zone - currentZone;
1175 if (isPowerOn() && currentZone == 1 && selectCommand == RotelCommand.RECORD_FONCTION_SELECT
1176 && !selectingRecord) {
1180 selectFeature(nbSelect, null, selectCommand);
1185 * Run a sequence of commands to display a particular feature on the device front panel
1187 * @param nbSelect the number of select commands to be run
1188 * @param preCmd the initial command to be sent to the device (before the select commands)
1189 * @param selectCmd the select command to be sent to the device
1191 * @throws RotelException in case of communication error with the device
1192 * @throws InterruptedException in case of interruption during a thread sleep
1194 private void selectFeature(int nbSelect, @Nullable RotelCommand preCmd, RotelCommand selectCmd)
1195 throws RotelException, InterruptedException {
1196 if (protocol == RotelProtocol.HEX) {
1197 if (preCmd != null) {
1198 sendCommand(preCmd);
1201 for (int i = 1; i <= nbSelect; i++) {
1202 sendCommand(selectCmd);
1209 * Open the connection with the Rotel device
1211 * @return true if the connection is opened successfully or flase if not
1213 private synchronized boolean openConnection() {
1214 protocolHandler.addEventListener(this);
1217 } catch (RotelException e) {
1218 logger.debug("openConnection() failed", e);
1220 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
1221 return connector.isConnected();
1225 * Close the connection with the Rotel device
1227 private synchronized void closeConnection() {
1229 protocolHandler.removeEventListener(this);
1230 logger.debug("closeConnection(): disconnected");
1234 public void onNewMessageEvent(EventObject event) {
1235 cancelPowerOffJob();
1237 RotelMessageEvent evt = (RotelMessageEvent) event;
1238 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
1240 String key = evt.getKey();
1241 String value = evt.getValue().trim();
1242 if (!KEY_ERROR.equals(key)) {
1243 updateStatus(ThingStatus.ONLINE);
1247 case KEY_INPUT_ZONE1:
1248 case KEY_VOLUME_ZONE1:
1249 case KEY_MUTE_ZONE1:
1250 case KEY_BASS_ZONE1:
1251 case KEY_TREBLE_ZONE1:
1252 case KEY_BALANCE_ZONE1:
1253 case KEY_FREQ_ZONE1:
1256 case KEY_POWER_ZONE2:
1257 case KEY_SOURCE_ZONE2:
1258 case KEY_INPUT_ZONE2:
1259 case KEY_VOLUME_ZONE2:
1260 case KEY_MUTE_ZONE2:
1261 case KEY_BASS_ZONE2:
1262 case KEY_TREBLE_ZONE2:
1263 case KEY_BALANCE_ZONE2:
1264 case KEY_FREQ_ZONE2:
1267 case KEY_POWER_ZONE3:
1268 case KEY_SOURCE_ZONE3:
1269 case KEY_INPUT_ZONE3:
1270 case KEY_VOLUME_ZONE3:
1271 case KEY_MUTE_ZONE3:
1272 case KEY_BASS_ZONE3:
1273 case KEY_TREBLE_ZONE3:
1274 case KEY_BALANCE_ZONE3:
1275 case KEY_FREQ_ZONE3:
1278 case KEY_POWER_ZONE4:
1279 case KEY_SOURCE_ZONE4:
1280 case KEY_INPUT_ZONE4:
1281 case KEY_VOLUME_ZONE4:
1282 case KEY_MUTE_ZONE4:
1283 case KEY_BASS_ZONE4:
1284 case KEY_TREBLE_ZONE4:
1285 case KEY_BALANCE_ZONE4:
1286 case KEY_FREQ_ZONE4:
1295 logger.debug("Reading feedback message failed");
1296 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1297 "@text/offline.comm-error-reading-thread");
1301 frontPanelLine1 = value;
1302 updateChannelState(CHANNEL_LINE1);
1305 frontPanelLine2 = value;
1306 updateChannelState(CHANNEL_LINE2);
1309 currentZone = Integer.parseInt(value);
1311 case KEY_RECORD_SEL:
1312 selectingRecord = MSG_VALUE_ON.equalsIgnoreCase(value);
1315 if (POWER_ON.equalsIgnoreCase(value)) {
1317 } else if (STANDBY.equalsIgnoreCase(value)) {
1319 if (model.getNumberOfZones() > 1 && !powerControlPerZone) {
1320 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1321 handlePowerOffZone(zone);
1324 } else if (POWER_OFF_DELAYED.equalsIgnoreCase(value)) {
1325 schedulePowerOffJob(false);
1327 throw new RotelException("Invalid value");
1330 case KEY_POWER_ZONE2:
1331 case KEY_POWER_ZONE3:
1332 case KEY_POWER_ZONE4:
1333 if (POWER_ON.equalsIgnoreCase(value)) {
1334 handlePowerOnZone(numZone);
1335 } else if (STANDBY.equalsIgnoreCase(value)) {
1336 handlePowerOffZone(numZone);
1338 throw new RotelException("Invalid value");
1341 case KEY_VOLUME_MIN:
1342 minVolume = Integer.parseInt(value);
1343 if (!model.hasDirectVolumeControl()) {
1344 logger.info("Set minValue to {} for your sitemap widget attached to your volume item.",
1348 case KEY_VOLUME_MAX:
1349 maxVolume = Integer.parseInt(value);
1350 if (!model.hasDirectVolumeControl()) {
1351 logger.info("Set maxValue to {} for your sitemap widget attached to your volume item.",
1356 case KEY_VOLUME_ZONE1:
1357 case KEY_VOLUME_ZONE2:
1358 case KEY_VOLUME_ZONE3:
1359 case KEY_VOLUME_ZONE4:
1360 fixedVolumeZones[numZone] = false;
1361 if (MSG_VALUE_FIX.equalsIgnoreCase(value)) {
1362 fixedVolumeZones[numZone] = true;
1363 } else if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1364 volumes[numZone] = minVolume;
1365 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1366 volumes[numZone] = maxVolume;
1368 volumes[numZone] = Integer.parseInt(value);
1371 updateChannelState(CHANNEL_VOLUME);
1372 updateChannelState(CHANNEL_MAIN_VOLUME);
1373 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1375 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1376 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1380 case KEY_MUTE_ZONE1:
1381 case KEY_MUTE_ZONE2:
1382 case KEY_MUTE_ZONE3:
1383 case KEY_MUTE_ZONE4:
1384 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1385 mutes[numZone] = true;
1387 updateChannelState(CHANNEL_MUTE);
1388 updateChannelState(CHANNEL_MAIN_MUTE);
1390 updateGroupChannelState(numZone, CHANNEL_MUTE);
1392 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1393 mutes[numZone] = false;
1395 updateChannelState(CHANNEL_MUTE);
1396 updateChannelState(CHANNEL_MAIN_MUTE);
1398 updateGroupChannelState(numZone, CHANNEL_MUTE);
1401 throw new RotelException("Invalid value");
1405 maxToneLevel = Integer.parseInt(value);
1406 minToneLevel = -maxToneLevel;
1408 "Set minValue to {} and maxValue to {} for your sitemap widget attached to your bass or treble item.",
1409 minToneLevel, maxToneLevel);
1412 case KEY_BASS_ZONE1:
1413 case KEY_BASS_ZONE2:
1414 case KEY_BASS_ZONE3:
1415 case KEY_BASS_ZONE4:
1416 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1417 basses[numZone] = minToneLevel;
1418 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1419 basses[numZone] = maxToneLevel;
1421 basses[numZone] = Integer.parseInt(value);
1424 updateChannelState(CHANNEL_BASS);
1425 updateChannelState(CHANNEL_MAIN_BASS);
1427 updateGroupChannelState(numZone, CHANNEL_BASS);
1431 case KEY_TREBLE_ZONE1:
1432 case KEY_TREBLE_ZONE2:
1433 case KEY_TREBLE_ZONE3:
1434 case KEY_TREBLE_ZONE4:
1435 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1436 trebles[numZone] = minToneLevel;
1437 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1438 trebles[numZone] = maxToneLevel;
1440 trebles[numZone] = Integer.parseInt(value);
1443 updateChannelState(CHANNEL_TREBLE);
1444 updateChannelState(CHANNEL_MAIN_TREBLE);
1446 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1450 sources[0] = model.getSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1451 updateChannelState(CHANNEL_SOURCE);
1452 updateChannelState(CHANNEL_MAIN_SOURCE);
1455 recordSource = model.getRecordSourceFromCommand(RotelCommand.getFromAsciiCommand(value));
1456 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1458 case KEY_SOURCE_ZONE2:
1459 case KEY_SOURCE_ZONE3:
1460 case KEY_SOURCE_ZONE4:
1461 case KEY_INPUT_ZONE1:
1462 case KEY_INPUT_ZONE2:
1463 case KEY_INPUT_ZONE3:
1464 case KEY_INPUT_ZONE4:
1465 sources[numZone] = model.getZoneSourceFromCommand(RotelCommand.getFromAsciiCommand(value), numZone);
1466 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1469 if ("dolby_pliix_movie".equals(value)) {
1470 value = "dolby_plii_movie";
1471 } else if ("dolby_pliix_music".equals(value)) {
1472 value = "dolby_plii_music";
1473 } else if ("dolby_pliix_game".equals(value)) {
1474 value = "dolby_plii_game";
1476 dsp = model.getDspFromFeedback(value);
1477 logger.debug("DSP {}", dsp.getName());
1478 updateChannelState(CHANNEL_DSP);
1479 updateChannelState(CHANNEL_MAIN_DSP);
1481 case KEY1_PLAY_STATUS:
1482 case KEY2_PLAY_STATUS:
1483 if (PLAY.equalsIgnoreCase(value)) {
1484 playStatus = RotelPlayStatus.PLAYING;
1485 updateChannelState(CHANNEL_PLAY_CONTROL);
1486 } else if (PAUSE.equalsIgnoreCase(value)) {
1487 playStatus = RotelPlayStatus.PAUSED;
1488 updateChannelState(CHANNEL_PLAY_CONTROL);
1489 } else if (STOP.equalsIgnoreCase(value)) {
1490 playStatus = RotelPlayStatus.STOPPED;
1491 updateChannelState(CHANNEL_PLAY_CONTROL);
1493 throw new RotelException("Invalid value");
1497 RotelSource source = sources[0];
1498 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1499 track = Integer.parseInt(value);
1500 updateChannelState(CHANNEL_TRACK);
1504 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1506 updateChannelState(CHANNEL_RANDOM);
1507 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1509 updateChannelState(CHANNEL_RANDOM);
1511 throw new RotelException("Invalid value");
1515 if (TRACK.equalsIgnoreCase(value)) {
1516 repeatMode = RotelRepeatMode.TRACK;
1517 updateChannelState(CHANNEL_REPEAT);
1518 } else if (DISC.equalsIgnoreCase(value)) {
1519 repeatMode = RotelRepeatMode.DISC;
1520 updateChannelState(CHANNEL_REPEAT);
1521 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1522 repeatMode = RotelRepeatMode.OFF;
1523 updateChannelState(CHANNEL_REPEAT);
1525 throw new RotelException("Invalid value");
1529 case KEY_FREQ_ZONE1:
1530 case KEY_FREQ_ZONE2:
1531 case KEY_FREQ_ZONE3:
1532 case KEY_FREQ_ZONE4:
1533 if (MSG_VALUE_OFF.equalsIgnoreCase(value) || MSG_VALUE_NONE.equalsIgnoreCase(value)) {
1534 frequencies[numZone] = 0.0;
1536 // Suppress a potential ending "k" or "K"
1537 if (value.toUpperCase().endsWith("K")) {
1538 value = value.substring(0, value.length() - 1);
1540 frequencies[numZone] = Double.parseDouble(value);
1543 updateChannelState(CHANNEL_FREQUENCY);
1545 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1549 brightness = Integer.parseInt(value);
1550 updateChannelState(CHANNEL_BRIGHTNESS);
1551 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1553 case KEY_UPDATE_MODE:
1554 case KEY_DISPLAY_UPDATE:
1557 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1559 updateChannelState(CHANNEL_TCBYPASS);
1560 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1562 updateChannelState(CHANNEL_TCBYPASS);
1564 throw new RotelException("Invalid value");
1568 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1570 updateChannelState(CHANNEL_TCBYPASS);
1571 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1573 updateChannelState(CHANNEL_TCBYPASS);
1575 throw new RotelException("Invalid value");
1579 case KEY_BALANCE_ZONE1:
1580 case KEY_BALANCE_ZONE2:
1581 case KEY_BALANCE_ZONE3:
1582 case KEY_BALANCE_ZONE4:
1583 if (MSG_VALUE_MIN.equalsIgnoreCase(value)) {
1584 balances[numZone] = minBalanceLevel;
1585 } else if (MSG_VALUE_MAX.equalsIgnoreCase(value)) {
1586 balances[numZone] = maxBalanceLevel;
1587 } else if (value.toUpperCase().startsWith("L")) {
1588 balances[numZone] = -Integer.parseInt(value.substring(1));
1589 } else if (value.toUpperCase().startsWith("R")) {
1590 balances[numZone] = Integer.parseInt(value.substring(1));
1592 balances[numZone] = Integer.parseInt(value);
1595 updateChannelState(CHANNEL_BALANCE);
1597 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1601 if (MSG_VALUE_SPEAKER_A.equalsIgnoreCase(value)) {
1604 updateChannelState(CHANNEL_SPEAKER_A);
1605 updateChannelState(CHANNEL_SPEAKER_B);
1606 } else if (MSG_VALUE_SPEAKER_B.equalsIgnoreCase(value)) {
1609 updateChannelState(CHANNEL_SPEAKER_A);
1610 updateChannelState(CHANNEL_SPEAKER_B);
1611 } else if (MSG_VALUE_SPEAKER_AB.equalsIgnoreCase(value)) {
1614 updateChannelState(CHANNEL_SPEAKER_A);
1615 updateChannelState(CHANNEL_SPEAKER_B);
1616 } else if (MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1619 updateChannelState(CHANNEL_SPEAKER_A);
1620 updateChannelState(CHANNEL_SPEAKER_B);
1622 throw new RotelException("Invalid value");
1626 getThing().setProperty(Thing.PROPERTY_MODEL_ID, value);
1629 getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value);
1632 logger.debug("onNewMessageEvent: unhandled key {}", key);
1635 } catch (NumberFormatException | RotelException e) {
1636 logger.debug("Invalid value {} for key {}", value, key);
1641 * Handle the received information that device power (main zone) is ON
1643 private void handlePowerOn() {
1644 Boolean prev = powers[0];
1646 updateChannelState(CHANNEL_POWER);
1647 updateChannelState(CHANNEL_MAIN_POWER);
1648 updateChannelState(CHANNEL_ALL_POWER);
1649 if ((prev == null) || !prev) {
1650 schedulePowerOnJob();
1655 * Handle the received information that device power (main zone) is OFF
1657 private void handlePowerOff() {
1658 cancelPowerOnZoneJob(0);
1660 updateChannelState(CHANNEL_POWER);
1661 updateChannelState(CHANNEL_SOURCE);
1662 updateChannelState(CHANNEL_DSP);
1663 updateChannelState(CHANNEL_VOLUME);
1664 updateChannelState(CHANNEL_MUTE);
1665 updateChannelState(CHANNEL_BASS);
1666 updateChannelState(CHANNEL_TREBLE);
1667 updateChannelState(CHANNEL_PLAY_CONTROL);
1668 updateChannelState(CHANNEL_TRACK);
1669 updateChannelState(CHANNEL_RANDOM);
1670 updateChannelState(CHANNEL_REPEAT);
1671 updateChannelState(CHANNEL_FREQUENCY);
1672 updateChannelState(CHANNEL_BRIGHTNESS);
1673 updateChannelState(CHANNEL_TCBYPASS);
1674 updateChannelState(CHANNEL_BALANCE);
1675 updateChannelState(CHANNEL_SPEAKER_A);
1676 updateChannelState(CHANNEL_SPEAKER_B);
1678 updateChannelState(CHANNEL_MAIN_POWER);
1679 updateChannelState(CHANNEL_MAIN_SOURCE);
1680 updateChannelState(CHANNEL_MAIN_RECORD_SOURCE);
1681 updateChannelState(CHANNEL_MAIN_DSP);
1682 updateChannelState(CHANNEL_MAIN_VOLUME);
1683 updateChannelState(CHANNEL_MAIN_VOLUME_UP_DOWN);
1684 updateChannelState(CHANNEL_MAIN_MUTE);
1685 updateChannelState(CHANNEL_MAIN_BASS);
1686 updateChannelState(CHANNEL_MAIN_TREBLE);
1688 updateChannelState(CHANNEL_ALL_POWER);
1689 updateChannelState(CHANNEL_ALL_BRIGHTNESS);
1693 * Handle the received information that a zone power is ON
1695 private void handlePowerOnZone(int numZone) {
1696 Boolean prev = powers[numZone];
1697 powers[numZone] = true;
1698 updateGroupChannelState(numZone, CHANNEL_POWER);
1699 if ((prev == null) || !prev) {
1700 schedulePowerOnZoneJob(numZone, getVolumeDownCommand(numZone), getVolumeUpCommand(numZone));
1705 * Handle the received information that a zone power is OFF
1707 private void handlePowerOffZone(int numZone) {
1708 cancelPowerOnZoneJob(numZone);
1709 powers[numZone] = false;
1710 updateGroupChannelState(numZone, CHANNEL_POWER);
1711 updateGroupChannelState(numZone, CHANNEL_SOURCE);
1712 updateGroupChannelState(numZone, CHANNEL_VOLUME);
1713 updateGroupChannelState(numZone, CHANNEL_MUTE);
1714 updateGroupChannelState(numZone, CHANNEL_BASS);
1715 updateGroupChannelState(numZone, CHANNEL_TREBLE);
1716 updateGroupChannelState(numZone, CHANNEL_BALANCE);
1717 updateGroupChannelState(numZone, CHANNEL_FREQUENCY);
1718 updateGroupChannelState(numZone, CHANNEL_VOLUME_UP_DOWN);
1722 * Schedule the job that will consider the device as OFF if no new event is received before its running
1724 * @param switchOffAllZones true if all zones have to be considered as OFF
1726 private void schedulePowerOffJob(boolean switchOffAllZones) {
1727 logger.debug("Schedule power OFF job");
1728 cancelPowerOffJob();
1729 powerOffJob = scheduler.schedule(() -> {
1730 logger.debug("Power OFF job");
1732 if (switchOffAllZones) {
1733 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1734 handlePowerOffZone(zone);
1737 }, 2000, TimeUnit.MILLISECONDS);
1741 * Cancel the job that will consider the device as OFF
1743 private void cancelPowerOffJob() {
1744 ScheduledFuture<?> powerOffJob = this.powerOffJob;
1745 if (powerOffJob != null && !powerOffJob.isCancelled()) {
1746 powerOffJob.cancel(true);
1747 this.powerOffJob = null;
1752 * Schedule the job to run with a few seconds delay when the device power (main zone) switched ON
1754 private void schedulePowerOnJob() {
1755 logger.debug("Schedule power ON job");
1756 cancelPowerOnZoneJob(0);
1757 powerOnZoneJobs[0] = scheduler.schedule(() -> {
1758 synchronized (sequenceLock) {
1759 logger.debug("Power ON job");
1763 if (model.getRespNbChars() <= 13 && model.hasVolumeControl()) {
1764 sendCommand(getVolumeDownCommand(0));
1766 sendCommand(getVolumeUpCommand(0));
1769 if (model.getNumberOfZones() > 1) {
1770 if (currentZone != 1
1771 && model.getZoneSelectCmd() == RotelCommand.RECORD_FONCTION_SELECT) {
1772 selectZone(1, model.getZoneSelectCmd());
1773 } else if (!selectingRecord) {
1774 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1778 sendCommand(RotelCommand.RECORD_FONCTION_SELECT);
1781 if (model.hasToneControl()) {
1782 if (model == RotelModel.RSX1065) {
1783 // No tone control select command
1784 sendCommand(RotelCommand.TREBLE_DOWN);
1786 sendCommand(RotelCommand.TREBLE_UP);
1788 sendCommand(RotelCommand.BASS_DOWN);
1790 sendCommand(RotelCommand.BASS_UP);
1793 selectFeature(2, null, RotelCommand.TONE_CONTROL_SELECT);
1798 if (model != RotelModel.RAP1580 && model != RotelModel.RDD1580
1799 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1800 sendCommand(RotelCommand.UPDATE_AUTO);
1801 Thread.sleep(SLEEP_INTV);
1803 if (model.hasSourceControl()) {
1804 sendCommand(RotelCommand.SOURCE);
1805 Thread.sleep(SLEEP_INTV);
1807 if (model.hasVolumeControl() || model.hasToneControl()) {
1808 if (model.hasVolumeControl() && model != RotelModel.RAP1580
1809 && model != RotelModel.RSP1576 && model != RotelModel.RSP1582) {
1810 sendCommand(RotelCommand.VOLUME_GET_MIN);
1811 Thread.sleep(SLEEP_INTV);
1812 sendCommand(RotelCommand.VOLUME_GET_MAX);
1813 Thread.sleep(SLEEP_INTV);
1815 if (model.hasToneControl()) {
1816 sendCommand(RotelCommand.TONE_MAX);
1817 Thread.sleep(SLEEP_INTV);
1819 // Wait enough to be sure to get the min/max values requested just before
1821 if (model.hasVolumeControl()) {
1822 sendCommand(RotelCommand.VOLUME_GET);
1823 Thread.sleep(SLEEP_INTV);
1824 if (model != RotelModel.RA11 && model != RotelModel.RA12
1825 && model != RotelModel.RCX1500) {
1826 sendCommand(RotelCommand.MUTE);
1827 Thread.sleep(SLEEP_INTV);
1830 if (model.hasToneControl()) {
1831 sendCommand(RotelCommand.BASS);
1832 Thread.sleep(SLEEP_INTV);
1833 sendCommand(RotelCommand.TREBLE);
1834 Thread.sleep(SLEEP_INTV);
1835 if (model.canGetBypassStatus()) {
1836 sendCommand(RotelCommand.TONE_CONTROLS);
1837 Thread.sleep(SLEEP_INTV);
1841 if (model.hasBalanceControl()) {
1842 sendCommand(RotelCommand.BALANCE);
1843 Thread.sleep(SLEEP_INTV);
1845 if (model.hasPlayControl()) {
1846 RotelSource source = sources[0];
1847 if (model != RotelModel.RCD1570 && model != RotelModel.RCD1572
1848 && (model != RotelModel.RCX1500 || source == null
1849 || !source.getName().equals("CD"))) {
1850 sendCommand(RotelCommand.PLAY_STATUS);
1851 Thread.sleep(SLEEP_INTV);
1853 sendCommand(RotelCommand.CD_PLAY_STATUS);
1854 Thread.sleep(SLEEP_INTV);
1857 if (model.hasDspControl()) {
1858 sendCommand(RotelCommand.DSP_MODE);
1859 Thread.sleep(SLEEP_INTV);
1861 if (model.canGetFrequency()) {
1862 sendCommand(RotelCommand.FREQUENCY);
1863 Thread.sleep(SLEEP_INTV);
1865 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1866 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1867 Thread.sleep(SLEEP_INTV);
1869 if (model.hasSpeakerGroups()) {
1870 sendCommand(RotelCommand.SPEAKER);
1871 Thread.sleep(SLEEP_INTV);
1875 sendCommand(RotelCommand.UPDATE_AUTO);
1876 Thread.sleep(SLEEP_INTV);
1877 if (model.hasSourceControl()) {
1878 if (model.getNumberOfZones() > 1) {
1879 sendCommand(RotelCommand.INPUT);
1881 sendCommand(RotelCommand.SOURCE);
1883 Thread.sleep(SLEEP_INTV);
1885 if (model.hasVolumeControl()) {
1886 sendCommand(RotelCommand.VOLUME_GET);
1887 Thread.sleep(SLEEP_INTV);
1888 sendCommand(RotelCommand.MUTE);
1889 Thread.sleep(SLEEP_INTV);
1891 if (model.hasToneControl()) {
1892 sendCommand(RotelCommand.BASS);
1893 Thread.sleep(SLEEP_INTV);
1894 sendCommand(RotelCommand.TREBLE);
1895 Thread.sleep(SLEEP_INTV);
1896 if (model.canGetBypassStatus()) {
1897 sendCommand(RotelCommand.TCBYPASS);
1898 Thread.sleep(SLEEP_INTV);
1901 if (model.hasBalanceControl()) {
1902 sendCommand(RotelCommand.BALANCE);
1903 Thread.sleep(SLEEP_INTV);
1905 if (model.hasPlayControl()) {
1906 sendCommand(RotelCommand.PLAY_STATUS);
1907 Thread.sleep(SLEEP_INTV);
1908 RotelSource source = sources[0];
1909 if (source != null && source.getName().equals("CD") && !model.hasSourceControl()) {
1910 sendCommand(RotelCommand.TRACK);
1911 Thread.sleep(SLEEP_INTV);
1912 sendCommand(RotelCommand.RANDOM_MODE);
1913 Thread.sleep(SLEEP_INTV);
1914 sendCommand(RotelCommand.REPEAT_MODE);
1915 Thread.sleep(SLEEP_INTV);
1918 if (model.hasDspControl()) {
1919 sendCommand(RotelCommand.DSP_MODE);
1920 Thread.sleep(SLEEP_INTV);
1922 if (model.canGetFrequency()) {
1923 sendCommand(RotelCommand.FREQUENCY);
1924 Thread.sleep(SLEEP_INTV);
1926 if (model.hasDimmerControl() && model.canGetDimmerLevel()) {
1927 sendCommand(RotelCommand.DIMMER_LEVEL_GET);
1928 Thread.sleep(SLEEP_INTV);
1930 if (model.hasSpeakerGroups()) {
1931 sendCommand(RotelCommand.SPEAKER);
1932 Thread.sleep(SLEEP_INTV);
1934 sendCommand(RotelCommand.MODEL);
1935 Thread.sleep(SLEEP_INTV);
1936 sendCommand(RotelCommand.VERSION);
1937 Thread.sleep(SLEEP_INTV);
1940 } catch (RotelException e) {
1941 logger.debug("Init sequence failed: {}", e.getMessage());
1942 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1943 "@text/offline.comm-error-init-sequence");
1945 } catch (InterruptedException e) {
1946 logger.debug("Init sequence interrupted: {}", e.getMessage());
1947 Thread.currentThread().interrupt();
1950 }, 2500, TimeUnit.MILLISECONDS);
1954 * Schedule the job to run with a few seconds delay when the zone power switched ON
1956 private void schedulePowerOnZoneJob(int numZone, RotelCommand volumeDown, RotelCommand volumeUp) {
1957 logger.debug("Schedule power ON zone {} job", numZone);
1958 cancelPowerOnZoneJob(numZone);
1959 powerOnZoneJobs[numZone] = scheduler.schedule(() -> {
1960 synchronized (sequenceLock) {
1961 logger.debug("Power ON zone {} job", numZone);
1963 if (protocol == RotelProtocol.HEX && model.getNumberOfZones() >= numZone) {
1964 selectZone(numZone, model.getZoneSelectCmd());
1965 sendCommand(model.hasZoneCommands(numZone) ? volumeDown : RotelCommand.VOLUME_DOWN);
1967 sendCommand(model.hasZoneCommands(numZone) ? volumeUp : RotelCommand.VOLUME_UP);
1970 } catch (RotelException e) {
1971 logger.debug("Init sequence zone {} failed: {}", numZone, e.getMessage());
1972 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1973 String.format("@text/offline.comm-error-init-sequence-zone [\"%d\"]", numZone));
1975 } catch (InterruptedException e) {
1976 logger.debug("Init sequence zone {} interrupted: {}", numZone, e.getMessage());
1977 Thread.currentThread().interrupt();
1980 }, 2500, TimeUnit.MILLISECONDS);
1984 * Cancel the job scheduled when the device power (main zone) or a zone power switched ON
1986 private void cancelPowerOnZoneJob(int numZone) {
1987 ScheduledFuture<?> powerOnZoneJob = powerOnZoneJobs[numZone];
1988 if (powerOnZoneJob != null && !powerOnZoneJob.isCancelled()) {
1989 powerOnZoneJob.cancel(true);
1990 powerOnZoneJobs[numZone] = null;
1995 * Schedule the reconnection job
1997 private void scheduleReconnectJob() {
1998 logger.debug("Schedule reconnect job");
1999 cancelReconnectJob();
2000 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
2001 if (!connector.isConnected()) {
2002 logger.debug("Trying to reconnect...");
2005 String error = null;
2006 if (openConnection()) {
2007 synchronized (sequenceLock) {
2008 schedulePowerOffJob(true);
2010 sendCommand(model.getPowerStateCmd());
2011 } catch (RotelException e) {
2012 error = "@text/offline.comm-error-first-command-after-reconnection";
2013 logger.debug("First command after connection failed", e);
2014 cancelPowerOffJob();
2019 error = "@text/offline.comm-error-reconnection";
2021 if (error != null) {
2023 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
2024 handlePowerOffZone(zone);
2026 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
2028 updateStatus(ThingStatus.ONLINE);
2031 }, 1, POLLING_INTERVAL, TimeUnit.SECONDS);
2035 * Cancel the reconnection job
2037 private void cancelReconnectJob() {
2038 ScheduledFuture<?> reconnectJob = this.reconnectJob;
2039 if (reconnectJob != null && !reconnectJob.isCancelled()) {
2040 reconnectJob.cancel(true);
2041 this.reconnectJob = null;
2045 private void updateGroupChannelState(int numZone, String channel) {
2046 updateChannelState(String.format("zone%d#%s", numZone, channel));
2050 * Update the state of a channel
2052 * @param channel the channel
2054 private void updateChannelState(String channel) {
2055 if (!isLinked(channel)) {
2058 State state = UnDefType.UNDEF;
2059 RotelSource localSource;
2062 case CHANNEL_ZONE1_SOURCE:
2063 case CHANNEL_ZONE1_VOLUME:
2064 case CHANNEL_ZONE1_MUTE:
2065 case CHANNEL_ZONE1_BASS:
2066 case CHANNEL_ZONE1_TREBLE:
2067 case CHANNEL_ZONE1_BALANCE:
2068 case CHANNEL_ZONE1_FREQUENCY:
2071 case CHANNEL_ZONE2_POWER:
2072 case CHANNEL_ZONE2_SOURCE:
2073 case CHANNEL_ZONE2_VOLUME:
2074 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2075 case CHANNEL_ZONE2_MUTE:
2076 case CHANNEL_ZONE2_BASS:
2077 case CHANNEL_ZONE2_TREBLE:
2078 case CHANNEL_ZONE2_BALANCE:
2079 case CHANNEL_ZONE2_FREQUENCY:
2082 case CHANNEL_ZONE3_POWER:
2083 case CHANNEL_ZONE3_SOURCE:
2084 case CHANNEL_ZONE3_VOLUME:
2085 case CHANNEL_ZONE3_MUTE:
2086 case CHANNEL_ZONE3_BASS:
2087 case CHANNEL_ZONE3_TREBLE:
2088 case CHANNEL_ZONE3_BALANCE:
2089 case CHANNEL_ZONE3_FREQUENCY:
2092 case CHANNEL_ZONE4_POWER:
2093 case CHANNEL_ZONE4_SOURCE:
2094 case CHANNEL_ZONE4_VOLUME:
2095 case CHANNEL_ZONE4_MUTE:
2096 case CHANNEL_ZONE4_BASS:
2097 case CHANNEL_ZONE4_TREBLE:
2098 case CHANNEL_ZONE4_BALANCE:
2099 case CHANNEL_ZONE4_FREQUENCY:
2107 case CHANNEL_MAIN_POWER:
2108 case CHANNEL_ALL_POWER:
2109 case CHANNEL_ZONE2_POWER:
2110 case CHANNEL_ZONE3_POWER:
2111 case CHANNEL_ZONE4_POWER:
2112 Boolean powerZone = powers[numZone];
2113 if (powerZone != null) {
2114 state = OnOffType.from(powerZone.booleanValue());
2117 case CHANNEL_SOURCE:
2118 case CHANNEL_MAIN_SOURCE:
2119 case CHANNEL_ZONE1_SOURCE:
2120 case CHANNEL_ZONE2_SOURCE:
2121 case CHANNEL_ZONE3_SOURCE:
2122 case CHANNEL_ZONE4_SOURCE:
2123 localSource = sources[numZone];
2124 if (isPowerOn(numZone) && localSource != null) {
2125 state = new StringType(localSource.getName());
2128 case CHANNEL_MAIN_RECORD_SOURCE:
2129 localSource = recordSource;
2130 if (isPowerOn() && localSource != null) {
2131 state = new StringType(localSource.getName());
2135 case CHANNEL_MAIN_DSP:
2137 state = new StringType(dsp.getName());
2140 case CHANNEL_VOLUME:
2141 case CHANNEL_MAIN_VOLUME:
2142 case CHANNEL_ZONE1_VOLUME:
2143 case CHANNEL_ZONE2_VOLUME:
2144 case CHANNEL_ZONE3_VOLUME:
2145 case CHANNEL_ZONE4_VOLUME:
2146 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2147 long volumePct = Math
2148 .round((double) (volumes[numZone] - minVolume) / (double) (maxVolume - minVolume) * 100.0);
2149 state = new PercentType(BigDecimal.valueOf(volumePct));
2152 case CHANNEL_MAIN_VOLUME_UP_DOWN:
2153 case CHANNEL_ZONE2_VOLUME_UP_DOWN:
2154 if (isPowerOn(numZone) && !fixedVolumeZones[numZone]) {
2155 state = new DecimalType(volumes[numZone]);
2159 case CHANNEL_MAIN_MUTE:
2160 case CHANNEL_ZONE1_MUTE:
2161 case CHANNEL_ZONE2_MUTE:
2162 case CHANNEL_ZONE3_MUTE:
2163 case CHANNEL_ZONE4_MUTE:
2164 if (isPowerOn(numZone)) {
2165 state = OnOffType.from(mutes[numZone]);
2169 case CHANNEL_MAIN_BASS:
2170 case CHANNEL_ZONE1_BASS:
2171 case CHANNEL_ZONE2_BASS:
2172 case CHANNEL_ZONE3_BASS:
2173 case CHANNEL_ZONE4_BASS:
2174 if (isPowerOn(numZone)) {
2175 state = new DecimalType(basses[numZone]);
2178 case CHANNEL_TREBLE:
2179 case CHANNEL_MAIN_TREBLE:
2180 case CHANNEL_ZONE1_TREBLE:
2181 case CHANNEL_ZONE2_TREBLE:
2182 case CHANNEL_ZONE3_TREBLE:
2183 case CHANNEL_ZONE4_TREBLE:
2184 if (isPowerOn(numZone)) {
2185 state = new DecimalType(trebles[numZone]);
2189 if (isPowerOn() && track > 0) {
2190 state = new DecimalType(track);
2193 case CHANNEL_RANDOM:
2195 state = OnOffType.from(randomMode);
2198 case CHANNEL_REPEAT:
2200 state = new StringType(repeatMode.name());
2203 case CHANNEL_PLAY_CONTROL:
2205 switch (playStatus) {
2207 state = PlayPauseType.PLAY;
2211 state = PlayPauseType.PAUSE;
2216 case CHANNEL_FREQUENCY:
2217 case CHANNEL_ZONE1_FREQUENCY:
2218 case CHANNEL_ZONE2_FREQUENCY:
2219 case CHANNEL_ZONE3_FREQUENCY:
2220 case CHANNEL_ZONE4_FREQUENCY:
2221 if (isPowerOn(numZone) && frequencies[numZone] > 0.0) {
2222 state = new DecimalType(frequencies[numZone]);
2226 state = new StringType(frontPanelLine1);
2229 state = new StringType(frontPanelLine2);
2231 case CHANNEL_BRIGHTNESS:
2232 case CHANNEL_ALL_BRIGHTNESS:
2233 if (isPowerOn() && model.hasDimmerControl()) {
2234 long dimmerPct = Math.round((double) (brightness - model.getDimmerLevelMin())
2235 / (double) (model.getDimmerLevelMax() - model.getDimmerLevelMin()) * 100.0);
2236 state = new PercentType(BigDecimal.valueOf(dimmerPct));
2239 case CHANNEL_TCBYPASS:
2241 state = OnOffType.from(tcbypass);
2244 case CHANNEL_BALANCE:
2245 case CHANNEL_ZONE1_BALANCE:
2246 case CHANNEL_ZONE2_BALANCE:
2247 case CHANNEL_ZONE3_BALANCE:
2248 case CHANNEL_ZONE4_BALANCE:
2249 if (isPowerOn(numZone)) {
2250 state = new DecimalType(balances[numZone]);
2253 case CHANNEL_SPEAKER_A:
2255 state = OnOffType.from(speakera);
2258 case CHANNEL_SPEAKER_B:
2260 state = OnOffType.from(speakerb);
2266 updateState(channel, state);
2270 * Inform about the device / main zone power state
2272 * @return true if device / main zone power state is known and known as ON
2274 private boolean isPowerOn() {
2275 return isPowerOn(0);
2279 * Inform about the power state
2281 * @param numZone the zone number (1-4) or 0 for the device or main zone
2283 * @return true if power state is known and known as ON
2285 private boolean isPowerOn(int numZone) {
2286 if (numZone < 0 || numZone > MAX_NUMBER_OF_ZONES) {
2287 throw new IllegalArgumentException("numZone must be in range 0-" + MAX_NUMBER_OF_ZONES);
2289 Boolean power = powers[numZone];
2290 return (numZone > 0 && !powerControlPerZone) ? isPowerOn(0) : power != null && power.booleanValue();
2294 * Get the command to be used for POWER ON
2296 * @param numZone the zone number (2-4) or 0 for the device or main zone
2298 * @return the command
2300 private RotelCommand getPowerOnCommand(int numZone) {
2303 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_ON : RotelCommand.POWER_ON;
2305 return RotelCommand.ZONE2_POWER_ON;
2307 return RotelCommand.ZONE3_POWER_ON;
2309 return RotelCommand.ZONE4_POWER_ON;
2311 throw new IllegalArgumentException("No power ON command defined for zone " + numZone);
2316 * Get the command to be used for POWER OFF
2318 * @param numZone the zone number (2-4) or 0 for the device or main zone
2320 * @return the command
2322 private RotelCommand getPowerOffCommand(int numZone) {
2325 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_POWER_OFF : RotelCommand.POWER_OFF;
2327 return RotelCommand.ZONE2_POWER_OFF;
2329 return RotelCommand.ZONE3_POWER_OFF;
2331 return RotelCommand.ZONE4_POWER_OFF;
2333 throw new IllegalArgumentException("No power OFF command defined for zone " + numZone);
2338 * Get the command to be used for VOLUME UP
2340 * @param numZone the zone number (1-4) or 0 for the device or main zone
2342 * @return the command
2344 private RotelCommand getVolumeUpCommand(int numZone) {
2347 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_UP : RotelCommand.VOLUME_UP;
2349 return RotelCommand.ZONE1_VOLUME_UP;
2351 return RotelCommand.ZONE2_VOLUME_UP;
2353 return RotelCommand.ZONE3_VOLUME_UP;
2355 return RotelCommand.ZONE4_VOLUME_UP;
2357 throw new IllegalArgumentException("No VOLUME UP command defined for zone " + numZone);
2362 * Get the command to be used for VOLUME DOWN
2364 * @param numZone the zone number (1-4) or 0 for the device or main zone
2366 * @return the command
2368 private RotelCommand getVolumeDownCommand(int numZone) {
2371 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_VOLUME_DOWN
2372 : RotelCommand.VOLUME_DOWN;
2374 return RotelCommand.ZONE1_VOLUME_DOWN;
2376 return RotelCommand.ZONE2_VOLUME_DOWN;
2378 return RotelCommand.ZONE3_VOLUME_DOWN;
2380 return RotelCommand.ZONE4_VOLUME_DOWN;
2382 throw new IllegalArgumentException("No VOLUME DOWN command defined for zone " + numZone);
2387 * Get the command to be used for VOLUME SET
2389 * @param numZone the zone number (1-4) or 0 for the device
2391 * @return the command
2393 private RotelCommand getVolumeSetCommand(int numZone) {
2396 return RotelCommand.VOLUME_SET;
2398 return RotelCommand.ZONE1_VOLUME_SET;
2400 return RotelCommand.ZONE2_VOLUME_SET;
2402 return RotelCommand.ZONE3_VOLUME_SET;
2404 return RotelCommand.ZONE4_VOLUME_SET;
2406 throw new IllegalArgumentException("No VOLUME SET command defined for zone " + numZone);
2411 * Get the command to be used for MUTE ON
2413 * @param numZone the zone number (1-4) or 0 for the device or main zone
2415 * @return the command
2417 private RotelCommand getMuteOnCommand(int numZone) {
2420 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_ON : RotelCommand.MUTE_ON;
2422 return RotelCommand.ZONE1_MUTE_ON;
2424 return RotelCommand.ZONE2_MUTE_ON;
2426 return RotelCommand.ZONE3_MUTE_ON;
2428 return RotelCommand.ZONE4_MUTE_ON;
2430 throw new IllegalArgumentException("No MUTE ON command defined for zone " + numZone);
2435 * Get the command to be used for MUTE OFF
2437 * @param numZone the zone number (1-4) or 0 for the device or main zone
2439 * @return the command
2441 private RotelCommand getMuteOffCommand(int numZone) {
2444 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_OFF : RotelCommand.MUTE_OFF;
2446 return RotelCommand.ZONE1_MUTE_OFF;
2448 return RotelCommand.ZONE2_MUTE_OFF;
2450 return RotelCommand.ZONE3_MUTE_OFF;
2452 return RotelCommand.ZONE4_MUTE_OFF;
2454 throw new IllegalArgumentException("No MUTE OFF command defined for zone " + numZone);
2459 * Get the command to be used for MUTE TOGGLE
2461 * @param numZone the zone number (1-4) or 0 for the device or main zone
2463 * @return the command
2465 private RotelCommand getMuteToggleCommand(int numZone) {
2468 return model.hasOtherThanPrimaryCommands() ? RotelCommand.MAIN_ZONE_MUTE_TOGGLE
2469 : RotelCommand.MUTE_TOGGLE;
2471 return RotelCommand.ZONE1_MUTE_TOGGLE;
2473 return RotelCommand.ZONE2_MUTE_TOGGLE;
2475 return RotelCommand.ZONE3_MUTE_TOGGLE;
2477 return RotelCommand.ZONE4_MUTE_TOGGLE;
2479 throw new IllegalArgumentException("No MUTE TOGGLE command defined for zone " + numZone);
2484 * Get the command to be used for BASS UP
2486 * @param numZone the zone number (1-4) or 0 for the device
2488 * @return the command
2490 private RotelCommand getBassUpCommand(int numZone) {
2493 return RotelCommand.BASS_UP;
2495 return RotelCommand.ZONE1_BASS_UP;
2497 return RotelCommand.ZONE2_BASS_UP;
2499 return RotelCommand.ZONE3_BASS_UP;
2501 return RotelCommand.ZONE4_BASS_UP;
2503 throw new IllegalArgumentException("No BASS UP command defined for zone " + numZone);
2508 * Get the command to be used for BASS DOWN
2510 * @param numZone the zone number (1-4) or 0 for the device
2512 * @return the command
2514 private RotelCommand getBassDownCommand(int numZone) {
2517 return RotelCommand.BASS_DOWN;
2519 return RotelCommand.ZONE1_BASS_DOWN;
2521 return RotelCommand.ZONE2_BASS_DOWN;
2523 return RotelCommand.ZONE3_BASS_DOWN;
2525 return RotelCommand.ZONE4_BASS_DOWN;
2527 throw new IllegalArgumentException("No BASS DOWN command defined for zone " + numZone);
2532 * Get the command to be used for BASS SET
2534 * @param numZone the zone number (1-4) or 0 for the device
2536 * @return the command
2538 private RotelCommand getBassSetCommand(int numZone) {
2541 return RotelCommand.BASS_SET;
2543 return RotelCommand.ZONE1_BASS_SET;
2545 return RotelCommand.ZONE2_BASS_SET;
2547 return RotelCommand.ZONE3_BASS_SET;
2549 return RotelCommand.ZONE4_BASS_SET;
2551 throw new IllegalArgumentException("No BASS SET command defined for zone " + numZone);
2556 * Get the command to be used for TREBLE UP
2558 * @param numZone the zone number (1-4) or 0 for the device
2560 * @return the command
2562 private RotelCommand getTrebleUpCommand(int numZone) {
2565 return RotelCommand.TREBLE_UP;
2567 return RotelCommand.ZONE1_TREBLE_UP;
2569 return RotelCommand.ZONE2_TREBLE_UP;
2571 return RotelCommand.ZONE3_TREBLE_UP;
2573 return RotelCommand.ZONE4_TREBLE_UP;
2575 throw new IllegalArgumentException("No TREBLE UP command defined for zone " + numZone);
2580 * Get the command to be used for TREBLE DOWN
2582 * @param numZone the zone number (1-4) or 0 for the device
2584 * @return the command
2586 private RotelCommand getTrebleDownCommand(int numZone) {
2589 return RotelCommand.TREBLE_DOWN;
2591 return RotelCommand.ZONE1_TREBLE_DOWN;
2593 return RotelCommand.ZONE2_TREBLE_DOWN;
2595 return RotelCommand.ZONE3_TREBLE_DOWN;
2597 return RotelCommand.ZONE4_TREBLE_DOWN;
2599 throw new IllegalArgumentException("No TREBLE DOWN command defined for zone " + numZone);
2604 * Get the command to be used for TREBLE SET
2606 * @param numZone the zone number (1-4) or 0 for the device
2608 * @return the command
2610 private RotelCommand getTrebleSetCommand(int numZone) {
2613 return RotelCommand.TREBLE_SET;
2615 return RotelCommand.ZONE1_TREBLE_SET;
2617 return RotelCommand.ZONE2_TREBLE_SET;
2619 return RotelCommand.ZONE3_TREBLE_SET;
2621 return RotelCommand.ZONE4_TREBLE_SET;
2623 throw new IllegalArgumentException("No TREBLE SET command defined for zone " + numZone);
2628 * Get the command to be used for BALANCE LEFT
2630 * @param numZone the zone number (1-4) or 0 for the device
2632 * @return the command
2634 private RotelCommand getBalanceLeftCommand(int numZone) {
2637 return RotelCommand.BALANCE_LEFT;
2639 return RotelCommand.ZONE1_BALANCE_LEFT;
2641 return RotelCommand.ZONE2_BALANCE_LEFT;
2643 return RotelCommand.ZONE3_BALANCE_LEFT;
2645 return RotelCommand.ZONE4_BALANCE_LEFT;
2647 throw new IllegalArgumentException("No BALANCE LEFT command defined for zone " + numZone);
2652 * Get the command to be used for BALANCE RIGHT
2654 * @param numZone the zone number (1-4) or 0 for the device
2656 * @return the command
2658 private RotelCommand getBalanceRightCommand(int numZone) {
2661 return RotelCommand.BALANCE_RIGHT;
2663 return RotelCommand.ZONE1_BALANCE_RIGHT;
2665 return RotelCommand.ZONE2_BALANCE_RIGHT;
2667 return RotelCommand.ZONE3_BALANCE_RIGHT;
2669 return RotelCommand.ZONE4_BALANCE_RIGHT;
2671 throw new IllegalArgumentException("No BALANCE RIGHT command defined for zone " + numZone);
2676 * Get the command to be used for BALANCE SET
2678 * @param numZone the zone number (1-4) or 0 for the device
2680 * @return the command
2682 private RotelCommand getBalanceSetCommand(int numZone) {
2685 return RotelCommand.BALANCE_SET;
2687 return RotelCommand.ZONE1_BALANCE_SET;
2689 return RotelCommand.ZONE2_BALANCE_SET;
2691 return RotelCommand.ZONE3_BALANCE_SET;
2693 return RotelCommand.ZONE4_BALANCE_SET;
2695 throw new IllegalArgumentException("No BALANCE SET command defined for zone " + numZone);
2699 private void sendCommand(RotelCommand cmd) throws RotelException {
2700 sendCommand(cmd, null);
2704 * Request the Rotel device to execute a command
2706 * @param cmd the command to execute
2707 * @param value the integer value to consider for volume, bass or treble adjustment
2709 * @throws RotelException - In case of any problem
2711 private void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
2714 message = protocolHandler.buildCommandMessage(cmd, value);
2715 } catch (RotelException e) {
2716 // Command not supported
2717 logger.debug("sendCommand: {}", e.getMessage());
2720 connector.writeOutput(cmd.getName(), message);
2722 if (connector instanceof RotelSimuConnector) {
2723 if ((protocol == RotelProtocol.HEX && cmd.getHexType() != 0)
2724 || (protocol == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
2725 || (protocol == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
2726 ((RotelSimuConnector) connector).buildFeedbackMessage(cmd, value);